From 6a7b71e8fbf6a9b379510be8f3d1773a136e06e8 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Wed, 11 Nov 2020 19:47:03 +0100 Subject: [PATCH 001/430] Bump pycsspeechtts to 1.0.4 (#43105) --- homeassistant/components/microsoft/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 0e371199a18..5b936bc7ded 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -2,6 +2,6 @@ "domain": "microsoft", "name": "Microsoft Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/microsoft", - "requirements": ["pycsspeechtts==1.0.3"], + "requirements": ["pycsspeechtts==1.0.4"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 29cc2164b10..2ce16daee72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1322,7 +1322,7 @@ pycoolmasternet-async==0.1.2 pycountry==19.8.18 # homeassistant.components.microsoft -pycsspeechtts==1.0.3 +pycsspeechtts==1.0.4 # homeassistant.components.cups # pycups==1.9.73 From 434d39a5ea7671d4489af523337ba562166cfddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schmitz=20von=20H=C3=BClst?= Date: Wed, 11 Nov 2020 20:03:55 +0100 Subject: [PATCH 002/430] Add initial rest query params (#42198) * add initial rest query params * of course I didn't run black * fix tests * fix tests * add test * reformat * add binary sensor test * fix tests * add one more test and fix switch * should not have touched that * if you don't pay attention once --- homeassistant/components/pvoutput/sensor.py | 2 +- .../components/rest/binary_sensor.py | 7 +++++- homeassistant/components/rest/data.py | 3 +++ homeassistant/components/rest/notify.py | 10 +++++++- homeassistant/components/rest/sensor.py | 7 +++++- homeassistant/components/rest/switch.py | 12 +++++++++- homeassistant/components/scrape/sensor.py | 2 +- homeassistant/const.py | 1 + tests/components/rest/test_binary_sensor.py | 23 +++++++++++++++++++ tests/components/rest/test_sensor.py | 23 +++++++++++++++++++ tests/components/rest/test_switch.py | 22 ++++++++++++++++++ 11 files changed, 106 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 0bc2a6f6ca8..fb3446fb652 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -53,7 +53,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= verify_ssl = DEFAULT_VERIFY_SSL headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} - rest = RestData(method, _ENDPOINT, auth, headers, payload, verify_ssl) + rest = RestData(method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() if rest.data is None: diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index c19bfe307d0..7f0f920b843 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_HEADERS, CONF_METHOD, CONF_NAME, + CONF_PARAMS, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, @@ -45,6 +46,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), vol.Optional(CONF_HEADERS): {cv.string: cv.string}, + vol.Optional(CONF_PARAMS): {cv.string: cv.string}, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(["POST", "GET"]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -78,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) headers = config.get(CONF_HEADERS) + params = config.get(CONF_PARAMS) device_class = config.get(CONF_DEVICE_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) force_update = config.get(CONF_FORCE_UPDATE) @@ -97,7 +100,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl, timeout) + rest = RestData( + method, resource, auth, headers, params, payload, verify_ssl, timeout + ) await rest.async_update() if rest.data is None: raise PlatformNotReady diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 9d9e802c2a0..bd35383e981 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -17,6 +17,7 @@ class RestData: resource, auth, headers, + params, data, verify_ssl, timeout=DEFAULT_TIMEOUT, @@ -26,6 +27,7 @@ class RestData: self._resource = resource self._auth = auth self._headers = headers + self._params = params self._request_data = data self._timeout = timeout self._verify_ssl = verify_ssl @@ -53,6 +55,7 @@ class RestData: self._method, self._resource, headers=self._headers, + params=self._params, auth=self._auth, data=self._request_data, timeout=self._timeout, diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index b8f81b19e92..3e4f97d5bc7 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_HEADERS, CONF_METHOD, CONF_NAME, + CONF_PARAMS, CONF_PASSWORD, CONF_RESOURCE, CONF_USERNAME, @@ -51,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ["POST", "GET", "POST_JSON"] ), vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string, vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string, @@ -75,6 +77,7 @@ def get_service(hass, config, discovery_info=None): resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) headers = config.get(CONF_HEADERS) + params = config.get(CONF_PARAMS) message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME) title_param_name = config.get(CONF_TITLE_PARAMETER_NAME) target_param_name = config.get(CONF_TARGET_PARAMETER_NAME) @@ -97,6 +100,7 @@ def get_service(hass, config, discovery_info=None): resource, method, headers, + params, message_param_name, title_param_name, target_param_name, @@ -116,6 +120,7 @@ class RestNotificationService(BaseNotificationService): resource, method, headers, + params, message_param_name, title_param_name, target_param_name, @@ -129,6 +134,7 @@ class RestNotificationService(BaseNotificationService): self._hass = hass self._method = method.upper() self._headers = headers + self._params = params self._message_param_name = message_param_name self._title_param_name = title_param_name self._target_param_name = target_param_name @@ -171,6 +177,7 @@ class RestNotificationService(BaseNotificationService): response = requests.post( self._resource, headers=self._headers, + params=self._params, data=data, timeout=10, auth=self._auth, @@ -180,6 +187,7 @@ class RestNotificationService(BaseNotificationService): response = requests.post( self._resource, headers=self._headers, + params=self._params, json=data, timeout=10, auth=self._auth, @@ -189,7 +197,7 @@ class RestNotificationService(BaseNotificationService): response = requests.get( self._resource, headers=self._headers, - params=data, + params=self._params.update(data), timeout=10, auth=self._auth, verify=self._verify_ssl, diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 826160604ba..f048eaa3b47 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_HEADERS, CONF_METHOD, CONF_NAME, + CONF_PARAMS, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, @@ -56,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -90,6 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) headers = config.get(CONF_HEADERS) + params = config.get(CONF_PARAMS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) device_class = config.get(CONF_DEVICE_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -112,7 +115,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= auth = (username, password) else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl, timeout) + rest = RestData( + method, resource, auth, headers, params, payload, verify_ssl, timeout + ) await rest.async_update() if rest.data is None: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 1b980e12b75..b6bd759d0bf 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_HEADERS, CONF_METHOD, CONF_NAME, + CONF_PARAMS, CONF_PASSWORD, CONF_RESOURCE, CONF_TIMEOUT, @@ -46,6 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_STATE_RESOURCE): cv.url, vol.Optional(CONF_HEADERS): {cv.string: cv.string}, + vol.Optional(CONF_PARAMS): {cv.string: cv.string}, vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template, vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template, vol.Optional(CONF_IS_ON_TEMPLATE): cv.template, @@ -71,6 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= is_on_template = config.get(CONF_IS_ON_TEMPLATE) method = config.get(CONF_METHOD) headers = config.get(CONF_HEADERS) + params = config.get(CONF_PARAMS) name = config.get(CONF_NAME) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -97,6 +100,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_resource, method, headers, + params, auth, body_on, body_off, @@ -129,6 +133,7 @@ class RestSwitch(SwitchEntity): state_resource, method, headers, + params, auth, body_on, body_off, @@ -143,6 +148,7 @@ class RestSwitch(SwitchEntity): self._state_resource = state_resource self._method = method self._headers = headers + self._params = params self._auth = auth self._body_on = body_on self._body_off = body_off @@ -201,6 +207,7 @@ class RestSwitch(SwitchEntity): auth=self._auth, data=bytes(body, "utf-8"), headers=self._headers, + params=self._params, ) return req @@ -219,7 +226,10 @@ class RestSwitch(SwitchEntity): with async_timeout.timeout(self._timeout): req = await websession.get( - self._state_resource, auth=self._auth, headers=self._headers + self._state_resource, + auth=self._auth, + headers=self._headers, + params=self._params, ) text = await req.text() diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 613151511ae..b76995fe39f 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -78,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= auth = HTTPBasicAuth(username, password) else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl) + rest = RestData(method, resource, auth, headers, None, payload, verify_ssl) await rest.async_update() if rest.data is None: diff --git a/homeassistant/const.py b/homeassistant/const.py index 173a39080b7..84a24ab1bad 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -128,6 +128,7 @@ CONF_NAME = "name" CONF_OFFSET = "offset" CONF_OPTIMISTIC = "optimistic" CONF_PACKAGES = "packages" +CONF_PARAMS = "params" CONF_PASSWORD = "password" CONF_PATH = "path" CONF_PAYLOAD = "payload" diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 1f2c88f4278..48d13a716ab 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -377,5 +377,28 @@ async def test_reload(hass): assert hass.states.get("binary_sensor.rollout") +@respx.mock +async def test_setup_query_params(hass): + """Test setup with query params.""" + respx.get( + "http://localhost?search=something", + status_code=200, + ) + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + "binary_sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "params": {"search": "something"}, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + def _get_fixtures_base_path(): return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d841f69e45f..71bcbedda88 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -249,6 +249,29 @@ async def test_setup_get_xml(hass): assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_MEGABYTES +@respx.mock +async def test_setup_query_params(hass): + """Test setup with query params.""" + respx.get( + "http://localhost?search=something", + status_code=200, + ) + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "params": {"search": "something"}, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + @respx.mock async def test_update_with_json_attrs(hass): """Test attributes get extracted from a JSON result.""" diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index f21eea0e242..5e0c9fbeab3 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -8,6 +8,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( CONF_HEADERS, CONF_NAME, + CONF_PARAMS, CONF_PLATFORM, CONF_RESOURCE, CONTENT_TYPE_JSON, @@ -28,6 +29,7 @@ RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE HEADERS = {"Content-type": CONTENT_TYPE_JSON} AUTH = None +PARAMS = None async def test_setup_missing_config(hass): @@ -81,6 +83,25 @@ async def test_setup_minimum(hass, aioclient_mock): assert aioclient_mock.call_count == 1 +async def test_setup_query_params(hass, aioclient_mock): + """Test setup with query params.""" + aioclient_mock.get("http://localhost/?search=something", status=HTTP_OK) + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + CONF_PLATFORM: rest.DOMAIN, + CONF_RESOURCE: "http://localhost", + CONF_PARAMS: {"search": "something"}, + } + }, + ) + print(aioclient_mock) + assert aioclient_mock.call_count == 1 + + async def test_setup(hass, aioclient_mock): """Test setup with valid configuration.""" aioclient_mock.get("http://localhost", status=HTTP_OK) @@ -137,6 +158,7 @@ def _setup_test_switch(hass): STATE_RESOURCE, METHOD, HEADERS, + PARAMS, AUTH, body_on, body_off, From 37bcfd1d2f3d778dfce4a8984ba806036d11a49f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 11 Nov 2020 20:10:17 +0100 Subject: [PATCH 003/430] Simplify distance conversion (#43107) --- homeassistant/util/distance.py | 129 +++++++-------------------------- 1 file changed, 25 insertions(+), 104 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 3bb3c258516..0e0a060c49c 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -1,5 +1,6 @@ """Distance util functions.""" from numbers import Number +from typing import Callable, Dict from homeassistant.const import ( LENGTH, @@ -25,6 +26,28 @@ VALID_UNITS = [ LENGTH_YARD, ] +TO_METERS: Dict[str, Callable[[float], float]] = { + LENGTH_METERS: lambda meters: meters, + LENGTH_MILES: lambda miles: miles * 1609.344, + LENGTH_YARD: lambda yards: yards * 0.9144, + LENGTH_FEET: lambda feet: feet * 0.3048, + LENGTH_INCHES: lambda inches: inches * 0.0254, + LENGTH_KILOMETERS: lambda kilometers: kilometers * 1000, + LENGTH_CENTIMETERS: lambda centimeters: centimeters * 0.01, + LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001, +} + +METERS_TO: Dict[str, Callable[[float], float]] = { + LENGTH_METERS: lambda meters: meters, + LENGTH_MILES: lambda meters: meters * 0.000621371, + LENGTH_YARD: lambda meters: meters * 1.09361, + LENGTH_FEET: lambda meters: meters * 3.28084, + LENGTH_INCHES: lambda meters: meters * 39.3701, + LENGTH_KILOMETERS: lambda meters: meters * 0.001, + LENGTH_CENTIMETERS: lambda meters: meters * 100, + LENGTH_MILLIMETERS: lambda meters: meters * 1000, +} + def convert(value: float, unit_1: str, unit_2: str) -> float: """Convert one unit of measurement to another.""" @@ -39,108 +62,6 @@ def convert(value: float, unit_1: str, unit_2: str) -> float: if unit_1 == unit_2 or unit_1 not in VALID_UNITS: return value - meters: float = value + meters: float = TO_METERS[unit_1](value) - if unit_1 == LENGTH_MILES: - meters = __miles_to_meters(value) - elif unit_1 == LENGTH_YARD: - meters = __yards_to_meters(value) - elif unit_1 == LENGTH_FEET: - meters = __feet_to_meters(value) - elif unit_1 == LENGTH_INCHES: - meters = __inches_to_meters(value) - elif unit_1 == LENGTH_KILOMETERS: - meters = __kilometers_to_meters(value) - elif unit_1 == LENGTH_CENTIMETERS: - meters = __centimeters_to_meters(value) - elif unit_1 == LENGTH_MILLIMETERS: - meters = __millimeters_to_meters(value) - - result = meters - - if unit_2 == LENGTH_MILES: - result = __meters_to_miles(meters) - elif unit_2 == LENGTH_YARD: - result = __meters_to_yards(meters) - elif unit_2 == LENGTH_FEET: - result = __meters_to_feet(meters) - elif unit_2 == LENGTH_INCHES: - result = __meters_to_inches(meters) - elif unit_2 == LENGTH_KILOMETERS: - result = __meters_to_kilometers(meters) - elif unit_2 == LENGTH_CENTIMETERS: - result = __meters_to_centimeters(meters) - elif unit_2 == LENGTH_MILLIMETERS: - result = __meters_to_millimeters(meters) - - return result - - -def __miles_to_meters(miles: float) -> float: - """Convert miles to meters.""" - return miles * 1609.344 - - -def __yards_to_meters(yards: float) -> float: - """Convert yards to meters.""" - return yards * 0.9144 - - -def __feet_to_meters(feet: float) -> float: - """Convert feet to meters.""" - return feet * 0.3048 - - -def __inches_to_meters(inches: float) -> float: - """Convert inches to meters.""" - return inches * 0.0254 - - -def __kilometers_to_meters(kilometers: float) -> float: - """Convert kilometers to meters.""" - return kilometers * 1000 - - -def __centimeters_to_meters(centimeters: float) -> float: - """Convert centimeters to meters.""" - return centimeters * 0.01 - - -def __millimeters_to_meters(millimeters: float) -> float: - """Convert millimeters to meters.""" - return millimeters * 0.001 - - -def __meters_to_miles(meters: float) -> float: - """Convert meters to miles.""" - return meters * 0.000621371 - - -def __meters_to_yards(meters: float) -> float: - """Convert meters to yards.""" - return meters * 1.09361 - - -def __meters_to_feet(meters: float) -> float: - """Convert meters to feet.""" - return meters * 3.28084 - - -def __meters_to_inches(meters: float) -> float: - """Convert meters to inches.""" - return meters * 39.3701 - - -def __meters_to_kilometers(meters: float) -> float: - """Convert meters to kilometers.""" - return meters * 0.001 - - -def __meters_to_centimeters(meters: float) -> float: - """Convert meters to centimeters.""" - return meters * 100 - - -def __meters_to_millimeters(meters: float) -> float: - """Convert meters to millimeters.""" - return meters * 1000 + return METERS_TO[unit_2](meters) From 403514ccb38f8fba4319b91197b404ee35d3ed31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 11 Nov 2020 20:12:24 +0100 Subject: [PATCH 004/430] Add system health section for the Supervisor (#43074) --- homeassistant/components/hassio/__init__.py | 28 ++++- homeassistant/components/hassio/handler.py | 16 +++ homeassistant/components/hassio/manifest.json | 2 +- homeassistant/components/hassio/strings.json | 18 +++ .../components/hassio/system_health.py | 72 +++++++++++ .../components/hassio/translations/en.json | 19 ++- .../components/homeassistant/strings.json | 20 ++-- .../components/homeassistant/system_health.py | 16 ++- .../homeassistant/translations/en.json | 33 +++-- tests/components/hassio/test_handler.py | 33 +++++ tests/components/hassio/test_init.py | 22 ++-- tests/components/hassio/test_system_health.py | 113 ++++++++++++++++++ tests/components/onboarding/test_views.py | 6 + 13 files changed, 354 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/hassio/strings.json create mode 100644 homeassistant/components/hassio/system_health.py create mode 100644 tests/components/hassio/test_system_health.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 1180e0a01d2..6484c85b95e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -42,9 +42,11 @@ CONFIG_SCHEMA = vol.Schema( ) -DATA_INFO = "hassio_info" -DATA_HOST_INFO = "hassio_host_info" DATA_CORE_INFO = "hassio_core_info" +DATA_HOST_INFO = "hassio_host_info" +DATA_INFO = "hassio_info" +DATA_OS_INFO = "hassio_os_info" +DATA_SUPERVISOR_INFO = "hassio_supervisor_info" HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) SERVICE_ADDON_START = "addon_start" @@ -218,6 +220,26 @@ def get_host_info(hass): return hass.data.get(DATA_HOST_INFO) +@callback +@bind_hass +def get_supervisor_info(hass): + """Return Supervisor information. + + Async friendly. + """ + return hass.data.get(DATA_SUPERVISOR_INFO) + + +@callback +@bind_hass +def get_os_info(hass): + """Return OS information. + + Async friendly. + """ + return hass.data.get(DATA_OS_INFO) + + @callback @bind_hass def get_core_info(hass): @@ -358,6 +380,8 @@ async def async_setup(hass, config): hass.data[DATA_INFO] = await hassio.get_info() hass.data[DATA_HOST_INFO] = await hassio.get_host_info() hass.data[DATA_CORE_INFO] = await hassio.get_core_info() + hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() + hass.data[DATA_OS_INFO] = await hassio.get_os_info() except HassioAPIError as err: _LOGGER.warning("Can't read last version: %s", err) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 58dd9db6623..6bc3cb345a5 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -82,6 +82,14 @@ class HassIO: """ return self.send_command("/host/info", method="get") + @api_data + def get_os_info(self): + """Return data for the OS. + + This method return a coroutine. + """ + return self.send_command("/os/info", method="get") + @api_data def get_core_info(self): """Return data for Home Asssistant Core. @@ -90,6 +98,14 @@ class HassIO: """ return self.send_command("/core/info", method="get") + @api_data + def get_supervisor_info(self): + """Return data for the Supervisor. + + This method returns a coroutine. + """ + return self.send_command("/supervisor/info", method="get") + @api_data def get_addon_info(self, addon): """Return data for a Add-on. diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 193c9640cd5..ba969a4af3a 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -1,6 +1,6 @@ { "domain": "hassio", - "name": "Hass.io", + "name": "Home Assistant Supervisor", "documentation": "https://www.home-assistant.io/hassio", "dependencies": ["http"], "after_dependencies": ["panel_custom"], diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json new file mode 100644 index 00000000000..875a79a60d7 --- /dev/null +++ b/homeassistant/components/hassio/strings.json @@ -0,0 +1,18 @@ +{ + "system_health": { + "info": { + "board": "Board", + "disk_total": "Disk Total", + "disk_used": "Disk Used", + "docker_version": "Docker Version", + "healthy": "Healthy", + "host_os": "Host Operating System", + "installed_addons": "Installed Add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor Version", + "supported": "Supported", + "update_channel": "Update Channel", + "version_api": "Version API" + } + } +} diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py new file mode 100644 index 00000000000..530703d3e25 --- /dev/null +++ b/homeassistant/components/hassio/system_health.py @@ -0,0 +1,72 @@ +"""Provide info to system health.""" +import os + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +SUPERVISOR_PING = f"http://{os.environ['HASSIO']}/supervisor/ping" +OBSERVER_URL = f"http://{os.environ['HASSIO']}:4357" + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info, "/hassio") + + +async def system_health_info(hass: HomeAssistant): + """Get info for the info page.""" + info = hass.components.hassio.get_info() + host_info = hass.components.hassio.get_host_info() + supervisor_info = hass.components.hassio.get_supervisor_info() + + if supervisor_info.get("healthy"): + healthy = True + else: + healthy = { + "type": "failed", + "error": "Unhealthy", + "more_info": "/hassio/system", + } + + if supervisor_info.get("supported"): + supported = True + else: + supported = { + "type": "failed", + "error": "Unsupported", + "more_info": "/hassio/system", + } + + information = { + "host_os": host_info.get("operating_system"), + "update_channel": info.get("channel"), + "supervisor_version": info.get("supervisor"), + "docker_version": info.get("docker"), + "disk_total": f"{host_info.get('disk_total')} GB", + "disk_used": f"{host_info.get('disk_used')} GB", + "healthy": healthy, + "supported": supported, + } + + if info.get("hassos") is not None: + os_info = hass.components.hassio.get_os_info() + information["board"] = os_info.get("board") + + information["supervisor_api"] = system_health.async_check_can_reach_url( + hass, SUPERVISOR_PING, OBSERVER_URL + ) + information["version_api"] = system_health.async_check_can_reach_url( + hass, + f"https://version.home-assistant.io/{info.get('channel')}.json", + "/hassio/system", + ) + + information["installed_addons"] = ", ".join( + f"{addon['name']} ({addon['version']})" + for addon in supervisor_info.get("addons", []) + ) + + return information diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 981cb51c83a..875a79a60d7 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,3 +1,18 @@ { - "title": "Hass.io" -} \ No newline at end of file + "system_health": { + "info": { + "board": "Board", + "disk_total": "Disk Total", + "disk_used": "Disk Used", + "docker_version": "Docker Version", + "healthy": "Healthy", + "host_os": "Host Operating System", + "installed_addons": "Installed Add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor Version", + "supported": "Supported", + "update_channel": "Update Channel", + "version_api": "Version API" + } + } +} diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index e349cc1cb83..1aa414c4984 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -1,20 +1,16 @@ { "system_health": { "info": { - "installation_type": "Installation Type", - "version": "Version", - "dev": "Development", - "virtualenv": "Virtual Environment", - "python_version": "Python Version", - "docker": "Docker", "arch": "CPU Architecture", - "timezone": "Timezone", - "os_name": "Operating System Name", + "dev": "Development", + "docker": "Docker", + "installation_type": "Installation Type", + "os_name": "Operating System Family", "os_version": "Operating System Version", - "supervisor": "Supervisor", - "host_os": "Home Assistant OS", - "chassis": "Chassis", - "docker_version": "Docker" + "python_version": "Python Version", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" } } } diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index af1be3bd0a5..b0245d9beec 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -15,5 +15,17 @@ def async_register( async def system_health_info(hass): """Get info for the info page.""" info = await system_info.async_get_system_info(hass) - info.pop("hassio") - return info + + return { + "version": info.get("version"), + "installation_type": info.get("installation_type"), + "dev": info.get("dev"), + "hassio": info.get("hassio"), + "docker": info.get("docker"), + "virtualenv": info.get("virtualenv"), + "python_version": info.get("python_version"), + "os_name": info.get("os_name"), + "os_version": info.get("os_version"), + "arch": info.get("arch"), + "timezone": info.get("timezone"), + } diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 9885785fa4d..8e810ef2143 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,20 +1,17 @@ { - "system_health": { - "info": { - "arch": "CPU Architecture", - "chassis": "Chassis", - "dev": "Development", - "docker": "Docker", - "docker_version": "Docker", - "host_os": "Home Assistant OS", - "installation_type": "Installation Type", - "os_name": "Operating System Name", - "os_version": "Operating System Version", - "python_version": "Python Version", - "supervisor": "Supervisor", - "timezone": "Timezone", - "version": "Version", - "virtualenv": "Virtual Environment" - } + "system_health": { + "info": { + "arch": "CPU Architecture", + "dev": "Development", + "docker": "Docker", + "installation_type": "Installation Type", + "os_name": "Operating System Family", + "os_version": "Operating System Version", + "python_version": "Python Version", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" } -} \ No newline at end of file + }, + "title": "Home Assistant" +} diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 311fc6c7e8c..33fb00b4485 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -80,6 +80,39 @@ async def test_api_host_info(hassio_handler, aioclient_mock): assert data["operating_system"] == "Debian GNU/Linux 10 (buster)" +async def test_api_supervisor_info(hassio_handler, aioclient_mock): + """Test setup with API Supervisor info.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": {"supported": True, "version": "2020.11.1", "channel": "stable"}, + }, + ) + + data = await hassio_handler.get_supervisor_info() + assert aioclient_mock.call_count == 1 + assert data["supported"] + assert data["version"] == "2020.11.1" + assert data["channel"] == "stable" + + +async def test_api_os_info(hassio_handler, aioclient_mock): + """Test setup with API OS info.""" + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": {"board": "odroid-n2", "version": "2020.11.1"}, + }, + ) + + data = await hassio_handler.get_os_info() + assert aioclient_mock.call_count == 1 + assert data["board"] == "odroid-n2" + assert data["version"] == "2020.11.1" + + async def test_api_host_info_error(hassio_handler, aioclient_mock): """Test setup with API Home Assistant info error.""" aioclient_mock.get( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 62b4a4adbd2..214551bc3b7 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -44,6 +44,14 @@ def mock_all(aioclient_mock): "http://127.0.0.1/core/info", json={"result": "ok", "data": {"version_latest": "1.0.0"}}, ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) @@ -55,7 +63,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -94,7 +102,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -110,7 +118,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -122,7 +130,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -169,7 +177,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -183,7 +191,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -200,7 +208,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py new file mode 100644 index 00000000000..cd6cb2d939f --- /dev/null +++ b/tests/components/hassio/test_system_health.py @@ -0,0 +1,113 @@ +"""Test hassio system health.""" +import asyncio +import os + +from aiohttp import ClientError + +from homeassistant.setup import async_setup_component + +from .test_init import MOCK_ENVIRON + +from tests.async_mock import patch +from tests.common import get_system_health_info + + +async def test_hassio_system_health(hass, aioclient_mock): + """Test hassio system health.""" + aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="") + aioclient_mock.get("https://version.home-assistant.io/stable.json", text="") + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}} + ) + + hass.config.components.add("hassio") + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component(hass, "system_health", {}) + + hass.data["hassio_info"] = { + "channel": "stable", + "supervisor": "2020.11.1", + "docker": "19.0.3", + "hassos": True, + } + hass.data["hassio_host_info"] = { + "operating_system": "Home Assistant OS 5.9", + "disk_total": "32.0", + "disk_used": "30.0", + } + hass.data["hassio_os_info"] = {"board": "odroid-n2"} + hass.data["hassio_supervisor_info"] = { + "healthy": True, + "supported": True, + "addons": [{"name": "Awesome Addon", "version": "1.0.0"}], + } + + info = await get_system_health_info(hass, "hassio") + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == { + "board": "odroid-n2", + "disk_total": "32.0 GB", + "disk_used": "30.0 GB", + "docker_version": "19.0.3", + "healthy": True, + "host_os": "Home Assistant OS 5.9", + "installed_addons": "Awesome Addon (1.0.0)", + "supervisor_api": "ok", + "supervisor_version": "2020.11.1", + "supported": True, + "update_channel": "stable", + "version_api": "ok", + } + + +async def test_hassio_system_health_with_issues(hass, aioclient_mock): + """Test hassio system health.""" + aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="") + aioclient_mock.get("https://version.home-assistant.io/stable.json", exc=ClientError) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}} + ) + + hass.config.components.add("hassio") + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component(hass, "system_health", {}) + + hass.data["hassio_info"] = {"channel": "stable"} + hass.data["hassio_host_info"] = {} + hass.data["hassio_os_info"] = {} + hass.data["hassio_supervisor_info"] = { + "healthy": False, + "supported": False, + } + + info = await get_system_health_info(hass, "hassio") + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info["healthy"] == { + "error": "Unhealthy", + "more_info": "/hassio/system", + "type": "failed", + } + assert info["supported"] == { + "error": "Unsupported", + "more_info": "/hassio/system", + "type": "failed", + } + assert info["version_api"] == { + "error": "unreachable", + "more_info": "/hassio/system", + "type": "failed", + } diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index a4826c00328..73845aba7b2 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -72,6 +72,12 @@ async def mock_supervisor_fixture(hass, aioclient_mock): ), patch( "homeassistant.components.hassio.HassIO.get_host_info", return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_os_info", + return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": {}}, From d8b067ebf913c1cd1f7d370a8fa1db3913250212 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 11 Nov 2020 20:13:14 +0100 Subject: [PATCH 005/430] Add Shelly support for REST sensors (#40429) --- homeassistant/components/shelly/__init__.py | 48 ++++- .../components/shelly/binary_sensor.py | 33 ++++ homeassistant/components/shelly/const.py | 5 + homeassistant/components/shelly/cover.py | 4 +- homeassistant/components/shelly/entity.py | 178 +++++++++++++++++- homeassistant/components/shelly/light.py | 4 +- homeassistant/components/shelly/sensor.py | 31 +++ homeassistant/components/shelly/switch.py | 4 +- homeassistant/components/shelly/utils.py | 20 ++ 9 files changed, 311 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 1f6ecdbd031..6aa08286dc1 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,9 +24,12 @@ from homeassistant.helpers import ( ) from .const import ( + COAP, DATA_CONFIG_ENTRY, DOMAIN, POLLING_TIMEOUT_MULTIPLIER, + REST, + REST_SENSORS_UPDATE_INTERVAL, SETUP_ENTRY_TIMEOUT_SEC, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, @@ -82,10 +85,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (asyncio.TimeoutError, OSError) as err: raise ConfigEntryNotReady from err - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - entry.entry_id + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} + coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + COAP ] = ShellyDeviceWrapper(hass, entry, device) - await wrapper.async_setup() + await coap_wrapper.async_setup() + + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + REST + ] = ShellyDeviceRestWrapper(hass, device) for component in PLATFORMS: hass.async_create_task( @@ -169,6 +177,37 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device.shutdown() +class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): + """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" + + def __init__(self, hass, device: aioshelly.Device): + """Initialize the Shelly device wrapper.""" + + super().__init__( + hass, + _LOGGER, + name=device.settings["name"] or device.settings["device"]["hostname"], + update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL), + ) + self.device = device + + async def _async_update_data(self): + """Fetch data.""" + try: + async with async_timeout.timeout(5): + _LOGGER.debug( + "REST update for %s", self.device.settings["device"]["hostname"] + ) + return await self.device.update_status() + except OSError as err: + raise update_coordinator.UpdateFailed("Error fetching data") from err + + @property + def mac(self): + """Mac address of the device.""" + return self.device.settings["device"]["mac"] + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( @@ -180,6 +219,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: - hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id).shutdown() + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][COAP].shutdown() + hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 1460c62f153..4f771a9cc46 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -1,5 +1,6 @@ """Binary sensor for Shelly.""" from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, @@ -11,8 +12,11 @@ from homeassistant.components.binary_sensor import ( from .entity import ( BlockAttributeDescription, + RestAttributeDescription, ShellyBlockAttributeEntity, + ShellyRestAttributeEntity, async_setup_entry_attribute_entities, + async_setup_entry_rest, ) SENSORS = { @@ -48,6 +52,22 @@ SENSORS = { ), } +REST_SENSORS = { + "cloud": RestAttributeDescription( + name="Cloud", + device_class=DEVICE_CLASS_CONNECTIVITY, + default_enabled=False, + path="cloud/connected", + ), + "fwupdate": RestAttributeDescription( + name="Firmware update", + icon="mdi:update", + default_enabled=False, + path="update/has_update", + attributes={"description": "available version:", "path": "update/new_version"}, + ), +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" @@ -55,6 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor ) + await async_setup_entry_rest( + hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor + ) + class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): """Shelly binary sensor entity.""" @@ -63,3 +87,12 @@ class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): def is_on(self): """Return true if sensor state is on.""" return bool(self.attribute_value) + + +class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity): + """Shelly REST binary sensor entity.""" + + @property + def is_on(self): + """Return true if REST sensor state is on.""" + return bool(self.attribute_value) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 50af82c2b7d..d058a8c4588 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,11 +1,16 @@ """Constants for the Shelly integration.""" +COAP = "coap" DATA_CONFIG_ENTRY = "config_entry" DOMAIN = "shelly" +REST = "rest" # Used to calculate the timeout in "_async_update_data" used for polling data from devices. POLLING_TIMEOUT_MULTIPLIER = 1.2 +# Refresh interval for REST sensors +REST_SENSORS_UPDATE_INTERVAL = 60 + # Timeout used for initial entry setup in "async_setup_entry". SETUP_ENTRY_TIMEOUT_SEC = 10 diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index a65ab5a05af..6caa7d5132c 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -12,13 +12,13 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from . import ShellyDeviceWrapper -from .const import DATA_CONFIG_ENTRY, DOMAIN +from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up cover for device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] blocks = [block for block in wrapper.device.blocks if block.type == "roller"] if not blocks: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 314ee48cf7e..75070403c2c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -4,12 +4,60 @@ from typing import Any, Callable, Optional, Union import aioshelly +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback -from homeassistant.helpers import device_registry, entity +from homeassistant.helpers import device_registry, entity, update_coordinator -from . import ShellyDeviceWrapper -from .const import DATA_CONFIG_ENTRY, DOMAIN -from .utils import get_entity_name +from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper +from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST +from .utils import get_entity_name, get_rest_value_from_path + + +def temperature_unit(block_info: dict) -> str: + """Detect temperature unit.""" + if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + +def shelly_naming(self, block, entity_type: str): + """Naming for switch and sensors.""" + + entity_name = self.wrapper.name + if not block: + return f"{entity_name} {self.description.name}" + + channels = 0 + mode = block.type + "s" + if "num_outputs" in self.wrapper.device.shelly: + channels = self.wrapper.device.shelly["num_outputs"] + if ( + self.wrapper.model in ["SHSW-21", "SHSW-25"] + and self.wrapper.device.settings["mode"] == "roller" + ): + channels = 1 + if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly: + channels = self.wrapper.device.shelly["num_emeters"] + if channels > 1 and block.type != "device": + # Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release + if "name" in self.wrapper.device.settings[mode][int(block.channel)]: + entity_name = self.wrapper.device.settings[mode][int(block.channel)]["name"] + else: + entity_name = None + if not entity_name: + if self.wrapper.model == "SHEM-3": + base = ord("A") + else: + base = ord("1") + entity_name = f"{self.wrapper.name} channel {chr(int(block.channel)+base)}" + + if entity_type == "switch": + return entity_name + + if entity_type == "sensor": + return f"{entity_name} {self.description.name}" + + raise ValueError async def async_setup_entry_attribute_entities( @@ -18,7 +66,7 @@ async def async_setup_entry_attribute_entities( """Set up entities for block attributes.""" wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id - ] + ][COAP] blocks = [] for block in wrapper.device.blocks: @@ -44,6 +92,27 @@ async def async_setup_entry_attribute_entities( ) +async def async_setup_entry_rest( + hass, config_entry, async_add_entities, sensors, sensor_class +): + """Set up entities for REST sensors.""" + wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ][REST] + + entities = [] + for sensor_id in sensors: + _desc = sensors.get(sensor_id) + + if not wrapper.device.settings.get("sleep_mode"): + entities.append(_desc) + + if not entities: + return + + async_add_entities([sensor_class(wrapper, description) for description in entities]) + + @dataclass class BlockAttributeDescription: """Class to describe a sensor.""" @@ -60,6 +129,21 @@ class BlockAttributeDescription: ] = None +@dataclass +class RestAttributeDescription: + """Class to describe a REST sensor.""" + + path: str + name: str + # Callable = lambda attr_info: unit + icon: Optional[str] = None + unit: Union[None, str, Callable[[dict], str]] = None + value: Callable[[Any], Any] = lambda val: val + device_class: Optional[str] = None + default_enabled: bool = True + attributes: Optional[dict] = None + + class ShellyBlockEntity(entity.Entity): """Helper class to represent a block.""" @@ -133,7 +217,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit = unit self._unique_id = f"{super().unique_id}-{self.attribute}" - self._name = get_entity_name(wrapper, block, self.description.name) + self._name = shelly_naming(self, block, "sensor") @property def unique_id(self): @@ -187,3 +271,85 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return None return self.description.device_state_attributes(self.block) + + +class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): + """Class to load info from REST.""" + + def __init__( + self, wrapper: ShellyDeviceWrapper, description: RestAttributeDescription + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper) + self.wrapper = wrapper + self.description = description + + self._unit = self.description.unit + self._name = shelly_naming(self, None, "sensor") + self.path = self.description.path + self._attributes = self.description.attributes + + @property + def name(self): + """Name of sensor.""" + return self._name + + @property + def device_info(self): + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if it should be enabled by default.""" + return self.description.default_enabled + + @property + def available(self): + """Available.""" + return self.wrapper.last_update_success + + @property + def attribute_value(self): + """Attribute.""" + return get_rest_value_from_path( + self.wrapper.device.status, self.description.device_class, self.path + ) + + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self.description.unit + + @property + def device_class(self): + """Device class of sensor.""" + return self.description.device_class + + @property + def icon(self): + """Icon of sensor.""" + return self.description.icon + + @property + def unique_id(self): + """Return unique ID of entity.""" + return f"{self.wrapper.mac}-{self.description.path}" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + + if self._attributes is None: + return None + + _description = self._attributes.get("description") + _attribute_value = get_rest_value_from_path( + self.wrapper.device.status, + self.description.device_class, + self._attributes.get("path"), + ) + + return {_description: _attribute_value} diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 7c4af9cf1ae..b62b5ee5e8c 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -17,14 +17,14 @@ from homeassistant.util.color import ( ) from . import ShellyDeviceWrapper -from .const import DATA_CONFIG_ENTRY, DOMAIN +from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity from .utils import async_remove_entity_by_domain async def async_setup_entry(hass, config_entry, async_add_entities): """Set up lights for device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] blocks = [] for block in wrapper.device.blocks: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index ddd61b6b613..a9a2e8d8d6b 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -8,13 +8,17 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, VOLT, ) from .entity import ( BlockAttributeDescription, + RestAttributeDescription, ShellyBlockAttributeEntity, + ShellyRestAttributeEntity, async_setup_entry_attribute_entities, + async_setup_entry_rest, ) from .utils import temperature_unit @@ -142,12 +146,30 @@ SENSORS = { ("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE), } +REST_SENSORS = { + "rssi": RestAttributeDescription( + name="RSSI", + unit=SIGNAL_STRENGTH_DECIBELS, + device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, + default_enabled=False, + path="wifi_sta/rssi", + ), + "uptime": RestAttributeDescription( + name="Uptime", + device_class=sensor.DEVICE_CLASS_TIMESTAMP, + path="uptime", + ), +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" await async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, SENSORS, ShellySensor ) + await async_setup_entry_rest( + hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor + ) class ShellySensor(ShellyBlockAttributeEntity): @@ -157,3 +179,12 @@ class ShellySensor(ShellyBlockAttributeEntity): def state(self): """Return value of sensor.""" return self.attribute_value + + +class ShellyRestSensor(ShellyRestAttributeEntity): + """Represent a shelly REST sensor.""" + + @property + def state(self): + """Return value of sensor.""" + return self.attribute_value diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 48cb6d728e9..ee5ecc5d8f8 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -5,14 +5,14 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from . import ShellyDeviceWrapper -from .const import DATA_CONFIG_ENTRY, DOMAIN +from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity from .utils import async_remove_entity_by_domain async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] # In roller mode the relay blocks exist but do not contain required info if ( diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 36ce48b5421..d6907e55e00 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -1,9 +1,12 @@ """Shelly helpers functions.""" + +from datetime import datetime, timedelta import logging from typing import Optional import aioshelly +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import entity_registry @@ -73,3 +76,20 @@ def get_entity_name( entity_name = f"{entity_name} {description}" return entity_name + + +def get_rest_value_from_path(status, device_class, path: str): + """Parser for REST path from device status.""" + + if "/" not in path: + _attribute_value = status[path] + else: + _attribute_value = status[path.split("/")[0]][path.split("/")[1]] + if device_class == DEVICE_CLASS_TIMESTAMP: + last_boot = datetime.utcnow() - timedelta(seconds=_attribute_value) + _attribute_value = last_boot.replace(microsecond=0).isoformat() + + if "new_version" in path: + _attribute_value = _attribute_value.split("/")[1].split("@")[0] + + return _attribute_value From df5a8c4dac7037edde99ff4e599a0cfce2f7e145 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 11 Nov 2020 13:17:46 -0600 Subject: [PATCH 006/430] Use media player image proxy for roku media browser (#43070) --- .../components/media_player/__init__.py | 18 +++++++++--------- homeassistant/components/roku/browse_media.py | 12 +++++++----- homeassistant/components/roku/media_player.py | 18 ++++++++++++++++-- tests/components/roku/test_media_player.py | 8 ++------ 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 4fb0cdae33e..71db60baa2e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -900,6 +900,9 @@ class MediaPlayerEntity(Entity): except asyncio.TimeoutError: pass + if content is None: + _LOGGER.warning("Error retrieving proxied image from %s", url) + return content, content_type def get_browse_image_url( @@ -910,15 +913,12 @@ class MediaPlayerEntity(Entity): f"/api/media_player_proxy/{self.entity_id}/browse_media" f"/{media_content_type}/{media_content_id}" ) - url = str( - URL(url_path).with_query( - { - "token": self.access_token, - "media_image_id": media_image_id, - } - ) - ) - return url + + url_query = {"token": self.access_token} + if media_image_id: + url_query["media_image_id"] = media_image_id + + return str(URL(url_path).with_query(url_query)) class MediaPlayerImageView(HomeAssistantView): diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index b5be3e99d9a..8110174450e 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -34,7 +34,7 @@ EXPANDABLE_MEDIA_TYPES = [ ] -def build_item_response(coordinator, payload): +def build_item_response(coordinator, payload, get_thumbnail_url=None): """Create response payload for the provided media query.""" search_id = payload["search_id"] search_type = payload["search_type"] @@ -75,13 +75,13 @@ def build_item_response(coordinator, payload): title=title, can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, can_expand=True, - children=[item_payload(item, coordinator) for item in media], + children=[item_payload(item, coordinator, get_thumbnail_url) for item in media], children_media_class=children_media_class, thumbnail=thumbnail, ) -def item_payload(item, coordinator): +def item_payload(item, coordinator, get_thumbnail_url=None): """ Create response payload for a single media item. @@ -92,7 +92,8 @@ def item_payload(item, coordinator): if "app_id" in item: media_content_type = MEDIA_TYPE_APP media_content_id = item["app_id"] - thumbnail = coordinator.roku.app_icon_url(item["app_id"]) + if get_thumbnail_url: + thumbnail = get_thumbnail_url(media_content_type, media_content_id) elif "channel_number" in item: media_content_type = MEDIA_TYPE_CHANNEL media_content_id = item["channel_number"] @@ -115,7 +116,7 @@ def item_payload(item, coordinator): ) -def library_payload(coordinator): +def library_payload(coordinator, get_thumbnail_url=None): """ Create response payload to describe contents of a specific library. @@ -147,6 +148,7 @@ def library_payload(coordinator): item_payload( {"title": item["title"], "type": item["type"]}, coordinator, + get_thumbnail_url, ) ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 0e035106824..e1662972cf6 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -238,16 +238,30 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Emulate opening the search screen and entering the search keyword.""" await self.coordinator.roku.search(keyword) + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Fetch media browser image to serve via proxy.""" + if media_content_type == MEDIA_TYPE_APP and media_content_id: + image_url = self.coordinator.roku.app_icon_url(media_content_id) + return await self._async_fetch_image(image_url) + + return (None, None) + async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + + def _get_thumbnail_url(*args, **kwargs): + return self.get_browse_image_url(*args, **kwargs) + if media_content_type in [None, "library"]: - return library_payload(self.coordinator) + return library_payload(self.coordinator, _get_thumbnail_url) payload = { "search_type": media_content_type, "search_id": media_content_id, } - response = build_item_response(self.coordinator, payload) + response = build_item_response(self.coordinator, payload, _get_thumbnail_url) if response is None: raise BrowseError( diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index b4ce1811c91..23dd9dbc6c8 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -539,18 +539,14 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" assert ( - msg["result"]["children"][0]["thumbnail"] - == "http://192.168.1.161:8060/query/icon/tvinput.hdmi2" + "/browse_media/app/tvinput.hdmi2" in msg["result"]["children"][0]["thumbnail"] ) assert msg["result"]["children"][0]["can_play"] assert msg["result"]["children"][3]["title"] == "Roku Channel Store" assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][3]["media_content_id"] == "11" - assert ( - msg["result"]["children"][3]["thumbnail"] - == "http://192.168.1.161:8060/query/icon/11" - ) + assert "/browse_media/app/11" in msg["result"]["children"][3]["thumbnail"] assert msg["result"]["children"][3]["can_play"] # test channels From d47b3a5f4406866e42d64aabc3395d935016d96d Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Wed, 11 Nov 2020 14:36:16 -0500 Subject: [PATCH 007/430] Fix Aurora integration including externalizing API to PyPi and adding config_flow (#43045) Co-authored-by: Pawel --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/aurora/__init__.py | 129 ++++++++++++++ .../components/aurora/binary_sensor.py | 163 +++++------------- .../components/aurora/config_flow.py | 110 ++++++++++++ homeassistant/components/aurora/const.py | 13 ++ homeassistant/components/aurora/manifest.json | 4 +- homeassistant/components/aurora/strings.json | 26 +++ .../components/aurora/translations/en.json | 26 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/aurora/test_binary_sensor.py | 60 ------- tests/components/aurora/test_config_flow.py | 113 ++++++++++++ 14 files changed, 477 insertions(+), 178 deletions(-) create mode 100644 homeassistant/components/aurora/config_flow.py create mode 100644 homeassistant/components/aurora/const.py create mode 100644 homeassistant/components/aurora/strings.json create mode 100644 homeassistant/components/aurora/translations/en.json delete mode 100644 tests/components/aurora/test_binary_sensor.py create mode 100644 tests/components/aurora/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2827607a95a..29991388c08 100644 --- a/.coveragerc +++ b/.coveragerc @@ -65,6 +65,9 @@ omit = homeassistant/components/asterisk_mbox/* homeassistant/components/aten_pe/* homeassistant/components/atome/* + homeassistant/components/aurora/__init__.py + homeassistant/components/aurora/binary_sensor.py + homeassistant/components/aurora/const.py homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py diff --git a/CODEOWNERS b/CODEOWNERS index e0096e7f217..f6967a7ed79 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -48,6 +48,7 @@ homeassistant/components/atag/* @MatsNL homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs homeassistant/components/august/* @bdraco +homeassistant/components/aurora/* @djtimca homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 2b3caa06843..260a3bd735d 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -1 +1,130 @@ """The aurora component.""" + +import asyncio +from datetime import timedelta +import logging + +from auroranoaa import AuroraForecast + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + AURORA_API, + CONF_THRESHOLD, + COORDINATOR, + DEFAULT_POLLING_INTERVAL, + DEFAULT_THRESHOLD, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["binary_sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Aurora component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Aurora from a config entry.""" + + conf = entry.data + options = entry.options + + session = aiohttp_client.async_get_clientsession(hass) + api = AuroraForecast(session) + + longitude = conf[CONF_LONGITUDE] + latitude = conf[CONF_LATITUDE] + polling_interval = DEFAULT_POLLING_INTERVAL + threshold = options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD) + name = conf[CONF_NAME] + + coordinator = AuroraDataUpdateCoordinator( + hass=hass, + name=name, + polling_interval=polling_interval, + api=api, + latitude=latitude, + longitude=longitude, + threshold=threshold, + ) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = { + COORDINATOR: coordinator, + AURORA_API: api, + } + + 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 + + +class AuroraDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the NOAA Aurora API.""" + + def __init__( + self, + hass: HomeAssistant, + name: str, + polling_interval: int, + api: str, + latitude: float, + longitude: float, + threshold: float, + ): + """Initialize the data updater.""" + + super().__init__( + hass=hass, + logger=_LOGGER, + name=name, + update_interval=timedelta(minutes=polling_interval), + ) + + self.api = api + self.name = name + self.latitude = int(latitude) + self.longitude = int(longitude) + self.threshold = int(threshold) + + async def _async_update_data(self): + """Fetch the data from the NOAA Aurora Forecast.""" + + try: + return await self.api.get_forecast_data(self.longitude, self.latitude) + except ConnectionError as error: + raise UpdateFailed(f"Error updating from NOAA: {error}") from error diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 1d5a6e83ec1..82be366ce6d 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,146 +1,75 @@ """Support for aurora forecast data sensor.""" -from datetime import timedelta import logging -from math import floor -from aiohttp.hdrs import USER_AGENT -import requests -import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from . import AuroraDataUpdateCoordinator +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTRIBUTION, + COORDINATOR, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" -CONF_THRESHOLD = "forecast_threshold" -DEFAULT_DEVICE_CLASS = "visible" -DEFAULT_NAME = "Aurora Visibility" -DEFAULT_THRESHOLD = 75 +async def async_setup_entry(hass, entry, async_add_entries): + """Set up the binary_sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + name = coordinator.name -HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0" + entity = AuroraSensor(coordinator, name) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int, - } -) + async_add_entries([entity]) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the aurora sensor.""" - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Lat. or long. not set in Home Assistant config") - return False - - name = config[CONF_NAME] - threshold = config[CONF_THRESHOLD] - - try: - aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold) - aurora_data.update() - except requests.exceptions.HTTPError as error: - _LOGGER.error("Connection to aurora forecast service failed: %s", error) - return False - - add_entities([AuroraSensor(aurora_data, name)], True) - - -class AuroraSensor(BinarySensorEntity): +class AuroraSensor(CoordinatorEntity, BinarySensorEntity): """Implementation of an aurora sensor.""" - def __init__(self, aurora_data, name): - """Initialize the sensor.""" - self.aurora_data = aurora_data + def __init__(self, coordinator: AuroraDataUpdateCoordinator, name): + """Define the binary sensor for the Aurora integration.""" + super().__init__(coordinator=coordinator) + self._name = name + self.coordinator = coordinator + self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}" + + @property + def unique_id(self): + """Define the unique id based on the latitude and longitude.""" + return self._unique_id @property def name(self): """Return the name of the sensor.""" - return f"{self._name}" + return self._name @property def is_on(self): """Return true if aurora is visible.""" - return self.aurora_data.is_visible if self.aurora_data else False - - @property - def device_class(self): - """Return the class of this device.""" - return DEFAULT_DEVICE_CLASS + return self.coordinator.data > self.coordinator.threshold @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {} + return {"attribution": ATTRIBUTION} - if self.aurora_data: - attrs["visibility_level"] = self.aurora_data.visibility_level - attrs["message"] = self.aurora_data.is_visible_text - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - return attrs + @property + def icon(self): + """Return the icon for the sensor.""" + return "mdi:hazard-lights" - def update(self): - """Get the latest data from Aurora API and updates the states.""" - self.aurora_data.update() - - -class AuroraData: - """Get aurora forecast.""" - - def __init__(self, latitude, longitude, threshold): - """Initialize the data object.""" - self.latitude = latitude - self.longitude = longitude - self.headers = {USER_AGENT: HA_USER_AGENT} - self.threshold = int(threshold) - self.is_visible = None - self.is_visible_text = None - self.visibility_level = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the Aurora service.""" - try: - self.visibility_level = self.get_aurora_forecast() - if int(self.visibility_level) > self.threshold: - self.is_visible = True - self.is_visible_text = "visible!" - else: - self.is_visible = False - self.is_visible_text = "nothing's out" - - except requests.exceptions.HTTPError as error: - _LOGGER.error("Connection to aurora forecast service failed: %s", error) - return False - - def get_aurora_forecast(self): - """Get forecast data and parse for given long/lat.""" - raw_data = requests.get(URL, headers=self.headers, timeout=5).text - # We discard comment rows (#) - # We split the raw text by line (\n) - # For each line we trim leading spaces and split by spaces - forecast_table = [ - row.strip().split() - for row in raw_data.split("\n") - if not row.startswith("#") - ] - - # Convert lat and long for data points in table - # Assumes self.latitude belongs to [-90;90[ (South to North) - # Assumes self.longitude belongs to [-180;180[ (West to East) - # No assumptions made regarding the number of rows and columns - converted_latitude = floor((self.latitude + 90) * len(forecast_table) / 180) - converted_longitude = floor( - (self.longitude + 180) * len(forecast_table[converted_latitude]) / 360 - ) - - return forecast_table[converted_latitude][converted_longitude] + @property + def device_info(self): + """Define the device based on name.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, + ATTR_NAME: self.coordinator.name, + ATTR_MANUFACTURER: "NOAA", + ATTR_MODEL: "Aurora Visibility Sensor", + } diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py new file mode 100644 index 00000000000..37885cc87cf --- /dev/null +++ b/homeassistant/components/aurora/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for SpaceX Launches and Starman.""" +import logging + +from auroranoaa import AuroraForecast +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NOAA Aurora Integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + name = user_input[CONF_NAME] + longitude = user_input[CONF_LONGITUDE] + latitude = user_input[CONF_LATITUDE] + + session = aiohttp_client.async_get_clientsession(self.hass) + api = AuroraForecast(session=session) + + try: + await api.get_forecast_data(longitude, latitude) + except ConnectionError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + f"{DOMAIN}_{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"Aurora - {name}", data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required( + CONF_LONGITUDE, + default=self.hass.config.longitude, + ): vol.All( + vol.Coerce(float), + vol.Range(min=-180, max=180), + ), + vol.Required( + CONF_LATITUDE, + default=self.hass.config.latitude, + ): vol.All( + vol.Coerce(float), + vol.Range(min=-90, max=90), + ), + } + ), + errors=errors, + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options flow changes.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage options.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_THRESHOLD, + default=self.config_entry.options.get( + CONF_THRESHOLD, DEFAULT_THRESHOLD + ), + ): vol.All( + vol.Coerce(int), + vol.Range(min=0, max=100), + ), + } + ), + ) diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py new file mode 100644 index 00000000000..f4451de863d --- /dev/null +++ b/homeassistant/components/aurora/const.py @@ -0,0 +1,13 @@ +"""Constants for the Aurora integration.""" + +DOMAIN = "aurora" +COORDINATOR = "coordinator" +AURORA_API = "aurora_api" +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +DEFAULT_POLLING_INTERVAL = 5 +CONF_THRESHOLD = "forecast_threshold" +DEFAULT_THRESHOLD = 75 +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" +DEFAULT_NAME = "Aurora Visibility" diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 3e7a9359614..20f9e82dcb0 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -2,5 +2,7 @@ "domain": "aurora", "name": "Aurora", "documentation": "https://www.home-assistant.io/integrations/aurora", - "codeowners": [] + "config_flow": true, + "codeowners": ["@djtimca"], + "requirements": ["auroranoaa==0.0.1"] } diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json new file mode 100644 index 00000000000..31af19748d6 --- /dev/null +++ b/homeassistant/components/aurora/strings.json @@ -0,0 +1,26 @@ +{ + "title": "NOAA Aurora Sensor", + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "latitude": "[%key:common::config_flow::data::latitude%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Threshold (%)" + } + } + } + } + } \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/en.json b/homeassistant/components/aurora/translations/en.json new file mode 100644 index 00000000000..e3e36574608 --- /dev/null +++ b/homeassistant/components/aurora/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Threshold (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3ac7fbae020..167ece1bcad 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -21,6 +21,7 @@ FLOWS = [ "arcam_fmj", "atag", "august", + "aurora", "avri", "awair", "axis", diff --git a/requirements_all.txt b/requirements_all.txt index 2ce16daee72..d910f50a3e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -296,6 +296,9 @@ asyncpysupla==0.0.5 # homeassistant.components.aten_pe atenpdu==0.3.0 +# homeassistant.components.aurora +auroranoaa==0.0.1 + # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adaf2f94dfc..c00bb2bbc40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,6 +173,9 @@ arcam-fmj==0.5.3 # homeassistant.components.upnp async-upnp-client==0.14.13 +# homeassistant.components.aurora +auroranoaa==0.0.1 + # homeassistant.components.stream av==8.0.2 diff --git a/tests/components/aurora/test_binary_sensor.py b/tests/components/aurora/test_binary_sensor.py deleted file mode 100644 index d4eea423244..00000000000 --- a/tests/components/aurora/test_binary_sensor.py +++ /dev/null @@ -1,60 +0,0 @@ -"""The tests for the Aurora sensor platform.""" -import re - -from homeassistant.components.aurora import binary_sensor as aurora - -from tests.common import load_fixture - - -def test_setup_and_initial_state(hass, requests_mock): - """Test that the component is created and initialized as expected.""" - uri = re.compile(r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt") - requests_mock.get(uri, text=load_fixture("aurora.txt")) - - entities = [] - - def mock_add_entities(new_entities, update_before_add=False): - """Mock add entities.""" - if update_before_add: - for entity in new_entities: - entity.update() - - for entity in new_entities: - entities.append(entity) - - config = {"name": "Test", "forecast_threshold": 75} - aurora.setup_platform(hass, config, mock_add_entities) - - aurora_component = entities[0] - assert len(entities) == 1 - assert aurora_component.name == "Test" - assert aurora_component.device_state_attributes["visibility_level"] == "0" - assert aurora_component.device_state_attributes["message"] == "nothing's out" - assert not aurora_component.is_on - - -def test_custom_threshold_works(hass, requests_mock): - """Test that the config can take a custom forecast threshold.""" - uri = re.compile(r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt") - requests_mock.get(uri, text=load_fixture("aurora.txt")) - - entities = [] - - def mock_add_entities(new_entities, update_before_add=False): - """Mock add entities.""" - if update_before_add: - for entity in new_entities: - entity.update() - - for entity in new_entities: - entities.append(entity) - - config = {"name": "Test", "forecast_threshold": 1} - hass.config.longitude = 18.987 - hass.config.latitude = 69.648 - - aurora.setup_platform(hass, config, mock_add_entities) - - aurora_component = entities[0] - assert aurora_component.aurora_data.visibility_level == "16" - assert aurora_component.is_on diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py new file mode 100644 index 00000000000..4d611bd3272 --- /dev/null +++ b/tests/components/aurora/test_config_flow.py @@ -0,0 +1,113 @@ +"""Test the Aurora config flow.""" + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.aurora.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +DATA = { + "name": "Home", + "latitude": -10, + "longitude": 10.2, +} + + +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.aurora.config_flow.AuroraForecast.get_forecast_data", + return_value=True, + ), patch( + "homeassistant.components.aurora.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.aurora.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Aurora - Home" + assert result2["data"] == DATA + 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_cannot_connect(hass): + """Test if invalid response or no connection returned from the API.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.aurora.AuroraForecast.get_forecast_data", + side_effect=ConnectionError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_with_unknown_error(hass): + """Test with unknown error response from the API.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.aurora.AuroraForecast.get_forecast_data", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_option_flow(hass): + """Test option flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=DATA) + entry.add_to_hass(hass) + + assert not entry.options + + with patch("homeassistant.components.aurora.async_setup_entry", return_value=True): + result = await hass.config_entries.options.async_init( + entry.entry_id, + data=None, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"forecast_threshold": 65}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"]["forecast_threshold"] == 65 From 1c7080d5c5692e517e5eface7c13f4bf97ccba60 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 11 Nov 2020 23:32:46 +0100 Subject: [PATCH 008/430] Add save and delete WS commands to blueprints (#42907) Co-authored-by: Paulus Schoutsen --- homeassistant/components/blueprint/const.py | 1 + homeassistant/components/blueprint/errors.py | 8 + homeassistant/components/blueprint/models.py | 39 +++++ homeassistant/components/blueprint/schemas.py | 24 ++- .../components/blueprint/websocket_api.py | 116 +++++++++++-- tests/components/blueprint/test_models.py | 24 ++- .../blueprint/test_websocket_api.py | 160 +++++++++++++++++- 7 files changed, 349 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/blueprint/const.py b/homeassistant/components/blueprint/const.py index fe4ee5b7ce6..d9e3839f026 100644 --- a/homeassistant/components/blueprint/const.py +++ b/homeassistant/components/blueprint/const.py @@ -5,5 +5,6 @@ CONF_BLUEPRINT = "blueprint" CONF_USE_BLUEPRINT = "use_blueprint" CONF_INPUT = "input" CONF_SOURCE_URL = "source_url" +CONF_DESCRIPTION = "description" DOMAIN = "blueprint" diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index dff65b5263d..4a12fde1c26 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -78,3 +78,11 @@ class MissingPlaceholder(BlueprintWithNameException): blueprint_name, f"Missing placeholder {', '.join(sorted(placeholder_names))}", ) + + +class FileAlreadyExists(BlueprintWithNameException): + """Error when file already exists.""" + + def __init__(self, domain: str, blueprint_name: str) -> None: + """Initialize blueprint exception.""" + super().__init__(domain, blueprint_name, "Blueprint already exists") diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 06401a34d7d..1681b4ffd31 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -24,6 +24,7 @@ from .const import ( from .errors import ( BlueprintException, FailedToLoad, + FileAlreadyExists, InvalidBlueprint, InvalidBlueprintInputs, MissingPlaceholder, @@ -86,6 +87,10 @@ class Blueprint: if source_url is not None: self.data[CONF_BLUEPRINT][CONF_SOURCE_URL] = source_url + def yaml(self) -> str: + """Dump blueprint as YAML.""" + return yaml.dump(self.data) + class BlueprintInputs: """Inputs for a blueprint.""" @@ -229,3 +234,37 @@ class DomainBlueprints: inputs = BlueprintInputs(blueprint, config_with_blueprint) inputs.validate() return inputs + + async def async_remove_blueprint(self, blueprint_path: str) -> None: + """Remove a blueprint file.""" + path = pathlib.Path( + self.hass.config.path(BLUEPRINT_FOLDER, self.domain, blueprint_path) + ) + + await self.hass.async_add_executor_job(path.unlink) + self._blueprints[blueprint_path] = None + + def _create_file(self, blueprint: Blueprint, blueprint_path: str) -> None: + """Create blueprint file.""" + + path = pathlib.Path( + self.hass.config.path(BLUEPRINT_FOLDER, self.domain, blueprint_path) + ) + if path.exists(): + raise FileAlreadyExists(self.domain, blueprint_path) + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(blueprint.yaml()) + + async def async_add_blueprint( + self, blueprint: Blueprint, blueprint_path: str + ) -> None: + """Add a blueprint.""" + if not blueprint_path.endswith(".yaml"): + blueprint_path = f"{blueprint_path}.yaml" + + await self.hass.async_add_executor_job( + self._create_file, blueprint, blueprint_path + ) + + self._blueprints[blueprint_path] = blueprint diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index 275a2cf242a..e04bc99e4b7 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -7,7 +7,13 @@ from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from .const import CONF_BLUEPRINT, CONF_INPUT, CONF_USE_BLUEPRINT +from .const import ( + CONF_BLUEPRINT, + CONF_DESCRIPTION, + CONF_INPUT, + CONF_SOURCE_URL, + CONF_USE_BLUEPRINT, +) @callback @@ -22,14 +28,26 @@ def is_blueprint_instance_config(config: Any) -> bool: return isinstance(config, dict) and CONF_USE_BLUEPRINT in config +BLUEPRINT_INPUT_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): str, + vol.Optional(CONF_DESCRIPTION): str, + } +) + BLUEPRINT_SCHEMA = vol.Schema( { - # No definition yet for the inputs. vol.Required(CONF_BLUEPRINT): vol.Schema( { vol.Required(CONF_NAME): str, vol.Required(CONF_DOMAIN): str, - vol.Optional(CONF_INPUT, default=dict): {str: None}, + vol.Optional(CONF_SOURCE_URL): cv.url, + vol.Optional(CONF_INPUT, default=dict): { + str: vol.Any( + None, + BLUEPRINT_INPUT_SCHEMA, + ) + }, } ), }, diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 51bec0eb2a0..88aa00788be 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -1,5 +1,4 @@ """Websocket API for blueprint.""" -import asyncio import logging from typing import Dict, Optional @@ -8,10 +7,13 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.util import yaml from . import importer, models from .const import DOMAIN +from .errors import FileAlreadyExists _LOGGER = logging.getLogger(__package__) @@ -21,12 +23,15 @@ def async_setup(hass: HomeAssistant): """Set up the websocket API.""" websocket_api.async_register_command(hass, ws_list_blueprints) websocket_api.async_register_command(hass, ws_import_blueprint) + websocket_api.async_register_command(hass, ws_save_blueprint) + websocket_api.async_register_command(hass, ws_delete_blueprint) @websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/list", + vol.Required("domain"): cv.string, } ) async def ws_list_blueprints(hass, connection, msg): @@ -36,21 +41,19 @@ async def ws_list_blueprints(hass, connection, msg): ) results = {} - for domain, domain_results in zip( - domain_blueprints, - await asyncio.gather( - *[db.async_get_blueprints() for db in domain_blueprints.values()] - ), - ): - for path, value in domain_results.items(): - if isinstance(value, models.Blueprint): - domain_results[path] = { - "metadata": value.metadata, - } - else: - domain_results[path] = {"error": str(value)} + if msg["domain"] not in domain_blueprints: + connection.send_result(msg["id"], results) + return - results[domain] = domain_results + domain_results = await domain_blueprints[msg["domain"]].async_get_blueprints() + + for path, value in domain_results.items(): + if isinstance(value, models.Blueprint): + results[path] = { + "metadata": value.metadata, + } + else: + results[path] = {"error": str(value)} connection.send_result(msg["id"], results) @@ -84,3 +87,86 @@ async def ws_import_blueprint(hass, connection, msg): }, }, ) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "blueprint/save", + vol.Required("domain"): cv.string, + vol.Required("path"): cv.path, + vol.Required("yaml"): cv.string, + vol.Optional("source_url"): cv.url, + } +) +async def ws_save_blueprint(hass, connection, msg): + """Save a blueprint.""" + + path = msg["path"] + domain = msg["domain"] + + domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get( + DOMAIN, {} + ) + + if domain not in domain_blueprints: + connection.send_error( + msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" + ) + + try: + blueprint = models.Blueprint( + yaml.parse_yaml(msg["yaml"]), expected_domain=domain + ) + if "source_url" in msg: + blueprint.update_metadata(source_url=msg["source_url"]) + except HomeAssistantError as err: + connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) + return + + try: + await domain_blueprints[domain].async_add_blueprint(blueprint, path) + except FileAlreadyExists: + connection.send_error(msg["id"], "already_exists", "File already exists") + return + except OSError as err: + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) + return + + connection.send_result( + msg["id"], + ) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "blueprint/delete", + vol.Required("domain"): cv.string, + vol.Required("path"): cv.path, + } +) +async def ws_delete_blueprint(hass, connection, msg): + """Delete a blueprint.""" + + path = msg["path"] + domain = msg["domain"] + + domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get( + DOMAIN, {} + ) + + if domain not in domain_blueprints: + connection.send_error( + msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" + ) + + try: + await domain_blueprints[domain].async_remove_blueprint(path) + except OSError as err: + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) + return + + connection.send_result( + msg["id"], + ) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 56fe13599d7..c66ebcfceb6 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -17,7 +17,10 @@ def blueprint_1(): "blueprint": { "name": "Hello", "domain": "automation", - "input": {"test-placeholder": None}, + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + "input": { + "test-placeholder": {"name": "Name", "description": "Description"} + }, }, "example": Placeholder("test-placeholder"), } @@ -59,7 +62,8 @@ def test_blueprint_properties(blueprint_1): assert blueprint_1.metadata == { "name": "Hello", "domain": "automation", - "input": {"test-placeholder": None}, + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + "input": {"test-placeholder": {"name": "Name", "description": "Description"}}, } assert blueprint_1.domain == "automation" assert blueprint_1.name == "Hello" @@ -152,3 +156,19 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1): ) assert inputs.blueprint is blueprint_1 assert inputs.inputs == {"test-placeholder": None} + + +async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): + """Test DomainBlueprints.async_add_blueprint.""" + with patch.object(domain_bps, "_create_file") as create_file_mock: + # Should add extension when not present. + await domain_bps.async_add_blueprint(blueprint_1, "something") + assert create_file_mock.call_args[0][1] == ("something.yaml") + + await domain_bps.async_add_blueprint(blueprint_1, "something2.yaml") + assert create_file_mock.call_args[0][1] == ("something2.yaml") + + # Should be in cache. + with patch.object(domain_bps, "_load_blueprint") as mock_load: + assert await domain_bps.async_get_blueprint("something.yaml") == blueprint_1 + assert not mock_load.mock_calls diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 54fcc0a5891..2459b014c7b 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -6,6 +6,8 @@ import pytest from homeassistant.components import automation from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch + @pytest.fixture(autouse=True) async def setup_bp(hass): @@ -19,14 +21,14 @@ async def setup_bp(hass): async def test_list_blueprints(hass, hass_ws_client): """Test listing blueprints.""" client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "blueprint/list"}) + await client.send_json({"id": 5, "type": "blueprint/list", "domain": "automation"}) msg = await client.receive_json() assert msg["id"] == 5 assert msg["success"] blueprints = msg["result"] - assert blueprints.get("automation") == { + assert blueprints == { "test_event_service.yaml": { "metadata": { "domain": "automation", @@ -44,8 +46,23 @@ async def test_list_blueprints(hass, hass_ws_client): } -async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): +async def test_list_blueprints_non_existing_domain(hass, hass_ws_client): """Test listing blueprints.""" + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "blueprint/list", "domain": "not_existsing"} + ) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert msg["success"] + blueprints = msg["result"] + assert blueprints == {} + + +async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): + """Test importing blueprints.""" raw_data = Path( hass.config.path("blueprints/automation/test_event_service.yaml") ).read_text() @@ -80,3 +97,140 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): }, }, } + + +async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): + """Test saving blueprints.""" + raw_data = Path( + hass.config.path("blueprints/automation/test_event_service.yaml") + ).read_text() + + with patch("pathlib.Path.write_text") as write_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 6, + "type": "blueprint/save", + "path": "test_save", + "yaml": raw_data, + "domain": "automation", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 6 + assert msg["success"] + assert write_mock.mock_calls + assert write_mock.call_args[0] == ( + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !placeholder 'trigger_event'\naction:\n service: !placeholder 'service_to_call'\n", + ) + + +async def test_save_existing_file(hass, aioclient_mock, hass_ws_client): + """Test saving blueprints.""" + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 7, + "type": "blueprint/save", + "path": "test_event_service", + "yaml": 'blueprint: {name: "name", domain: "automation"}', + "domain": "automation", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 7 + assert not msg["success"] + assert msg["error"] == {"code": "already_exists", "message": "File already exists"} + + +async def test_save_file_error(hass, aioclient_mock, hass_ws_client): + """Test saving blueprints with OS error.""" + with patch("pathlib.Path.write_text", side_effect=OSError): + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 8, + "type": "blueprint/save", + "path": "test_save", + "yaml": "raw_data", + "domain": "automation", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 8 + assert not msg["success"] + + +async def test_save_invalid_blueprint(hass, aioclient_mock, hass_ws_client): + """Test saving invalid blueprints.""" + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 8, + "type": "blueprint/save", + "path": "test_wrong", + "yaml": "wrong_blueprint", + "domain": "automation", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 8 + assert not msg["success"] + assert msg["error"] == { + "code": "invalid_format", + "message": "Invalid blueprint: expected a dictionary. Got 'wrong_blueprint'", + } + + +async def test_delete_blueprint(hass, aioclient_mock, hass_ws_client): + """Test deleting blueprints.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_delete", + "domain": "automation", + } + ) + + msg = await client.receive_json() + + assert unlink_mock.mock_calls + assert msg["id"] == 9 + assert msg["success"] + + +async def test_delete_non_exist_file_blueprint(hass, aioclient_mock, hass_ws_client): + """Test deleting non existing blueprints.""" + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "none_existing", + "domain": "automation", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 9 + assert not msg["success"] From 8a0907acf9830883cca6c31ef42d3e738f231db1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 12 Nov 2020 00:10:58 +0000 Subject: [PATCH 009/430] [ci skip] Translation update --- .../components/acmeda/translations/cs.json | 7 ++++ .../components/airvisual/translations/cs.json | 3 ++ .../components/august/translations/cs.json | 2 ++ .../components/aurora/translations/ca.json | 26 ++++++++++++++ .../components/aurora/translations/cs.json | 26 ++++++++++++++ .../components/awair/translations/cs.json | 3 +- .../components/axis/translations/cs.json | 10 ++++++ .../binary_sensor/translations/lb.json | 16 +++++++++ .../components/canary/translations/cs.json | 1 + .../cert_expiry/translations/cs.json | 3 +- .../components/cloud/translations/ca.json | 16 +++++++++ .../components/cloud/translations/lb.json | 16 +++++++++ .../components/cloud/translations/no.json | 4 +-- .../components/control4/translations/cs.json | 10 ++++++ .../components/deconz/translations/cs.json | 1 + .../components/demo/translations/cs.json | 1 + .../components/denonavr/translations/cs.json | 12 ++++++- .../components/doorbird/translations/cs.json | 3 +- .../components/dsmr/translations/ca.json | 10 ++++++ .../components/dunehd/translations/cs.json | 1 + .../flick_electric/translations/cs.json | 1 + .../forked_daapd/translations/cs.json | 20 +++++++++-- .../components/goalzero/translations/cs.json | 1 + .../components/hassio/translations/ca.json | 16 +++++++++ .../components/hassio/translations/cs.json | 16 +++++++++ .../components/hassio/translations/en.json | 35 ++++++++++--------- .../homeassistant/translations/ca.json | 20 +++++++++++ .../homeassistant/translations/cs.json | 2 +- .../homeassistant/translations/en.json | 33 +++++++++-------- .../homekit_controller/translations/cs.json | 1 + .../huawei_lte/translations/cs.json | 1 + .../components/insteon/translations/cs.json | 13 +++++-- .../components/iqvia/translations/cs.json | 1 + .../components/isy994/translations/cs.json | 3 ++ .../components/kodi/translations/cs.json | 3 +- .../components/konnected/translations/cs.json | 3 +- .../components/lovelace/translations/ca.json | 9 +++++ .../components/netatmo/translations/cs.json | 8 +++-- .../components/plex/translations/cs.json | 3 ++ .../components/plugwise/translations/cs.json | 2 ++ .../components/rfxtrx/translations/cs.json | 8 ++++- .../components/roon/translations/cs.json | 1 + .../simplisafe/translations/cs.json | 2 ++ .../components/smappee/translations/cs.json | 2 ++ .../components/smarthab/translations/cs.json | 3 +- .../smartthings/translations/cs.json | 2 ++ .../components/soma/translations/cs.json | 3 +- .../components/syncthru/translations/cs.json | 3 ++ .../components/toon/translations/cs.json | 1 + .../transmission/translations/cs.json | 1 + .../components/vera/translations/cs.json | 1 + .../components/vilfo/translations/cs.json | 1 + .../components/vizio/translations/cs.json | 3 +- .../components/wolflink/translations/cs.json | 3 +- .../wolflink/translations/sensor.cs.json | 14 ++++++++ .../xiaomi_aqara/translations/cs.json | 5 ++- .../components/zha/translations/cs.json | 1 + 57 files changed, 363 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/aurora/translations/ca.json create mode 100644 homeassistant/components/aurora/translations/cs.json create mode 100644 homeassistant/components/cloud/translations/ca.json create mode 100644 homeassistant/components/cloud/translations/lb.json create mode 100644 homeassistant/components/homeassistant/translations/ca.json create mode 100644 homeassistant/components/lovelace/translations/ca.json diff --git a/homeassistant/components/acmeda/translations/cs.json b/homeassistant/components/acmeda/translations/cs.json index 3f0012e00d2..3f392ed0347 100644 --- a/homeassistant/components/acmeda/translations/cs.json +++ b/homeassistant/components/acmeda/translations/cs.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "step": { + "user": { + "data": { + "id": "ID hostitele" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json index 72e456806e0..5c26dcf98ef 100644 --- a/homeassistant/components/airvisual/translations/cs.json +++ b/homeassistant/components/airvisual/translations/cs.json @@ -44,6 +44,9 @@ "options": { "step": { "init": { + "data": { + "show_on_map": "Uk\u00e1zat monitorovanou oblast na map\u011b" + }, "title": "Nastavte AirVisual" } } diff --git a/homeassistant/components/august/translations/cs.json b/homeassistant/components/august/translations/cs.json index f1db40b0beb..4100014abb6 100644 --- a/homeassistant/components/august/translations/cs.json +++ b/homeassistant/components/august/translations/cs.json @@ -17,12 +17,14 @@ "timeout": "\u010casov\u00fd limit (v sekund\u00e1ch)", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, + "description": "Pokud je metoda p\u0159ihl\u00e1\u0161en\u00ed \"e-mail\", je e-mailovou adresou u\u017eivatelsk\u00e9 jm\u00e9no. Pokud je p\u0159ihla\u0161ovac\u00ed metoda \"telefon\", u\u017eivatelsk\u00e9 jm\u00e9no je telefonn\u00ed \u010d\u00edslo ve form\u00e1tu \"+NNNNNNNNN\".", "title": "Nastavte \u00fa\u010det August" }, "validation": { "data": { "code": "Ov\u011b\u0159ovac\u00ed k\u00f3d" }, + "description": "Zkontrolujte pros\u00edm {login_method} ({username}) a n\u00ed\u017ee zadejte ov\u011b\u0159ovac\u00ed k\u00f3d", "title": "Dvoufaktorov\u00e9 ov\u011b\u0159ov\u00e1n\u00ed" } } diff --git a/homeassistant/components/aurora/translations/ca.json b/homeassistant/components/aurora/translations/ca.json new file mode 100644 index 00000000000..99db9855e74 --- /dev/null +++ b/homeassistant/components/aurora/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Llindar (%)" + } + } + } + }, + "title": "Sensor Aurora NOAA" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/cs.json b/homeassistant/components/aurora/translations/cs.json new file mode 100644 index 00000000000..e7a10c94241 --- /dev/null +++ b/homeassistant/components/aurora/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Pr\u00e1h (%)" + } + } + } + }, + "title": "Senzor NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json index 5bc950b7dcf..dfc83778bf9 100644 --- a/homeassistant/components/awair/translations/cs.json +++ b/homeassistant/components/awair/translations/cs.json @@ -20,7 +20,8 @@ "data": { "access_token": "P\u0159\u00edstupov\u00fd token", "email": "E-mail" - } + }, + "description": "Pro p\u0159\u00edstupov\u00fd token v\u00fdvoj\u00e1\u0159e Awair se mus\u00edte zaregistrovat na: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/axis/translations/cs.json b/homeassistant/components/axis/translations/cs.json index a6060a13d21..fd99c68ab35 100644 --- a/homeassistant/components/axis/translations/cs.json +++ b/homeassistant/components/axis/translations/cs.json @@ -23,5 +23,15 @@ "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed Axis" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Vyberte profil streamu, kter\u00fd chcete pou\u017e\u00edt" + }, + "title": "Mo\u017enosti video streamu za\u0159\u00edzen\u00ed Axis" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/lb.json b/homeassistant/components/binary_sensor/translations/lb.json index 4c816d6424a..fc186bee447 100644 --- a/homeassistant/components/binary_sensor/translations/lb.json +++ b/homeassistant/components/binary_sensor/translations/lb.json @@ -98,6 +98,10 @@ "off": "Normal", "on": "Niddreg" }, + "battery_charging": { + "off": "Lued net", + "on": "Lued" + }, "cold": { "off": "Normal", "on": "Kal" @@ -122,6 +126,10 @@ "off": "Normal", "on": "Waarm" }, + "light": { + "off": "Keng Luucht", + "on": "Luucht detekt\u00e9iert" + }, "lock": { "off": "Gespaart", "on": "Net gespaart" @@ -134,6 +142,10 @@ "off": "Roueg", "on": "Detekt\u00e9iert" }, + "moving": { + "off": "Keng Beweegung", + "on": "Beweegung" + }, "occupancy": { "off": "Roueg", "on": "Detekt\u00e9iert" @@ -142,6 +154,10 @@ "off": "Zou", "on": "Op" }, + "plug": { + "off": "Net ugeschloss", + "on": "Ugeschloss" + }, "problem": { "off": "OK", "on": "Problem" diff --git a/homeassistant/components/canary/translations/cs.json b/homeassistant/components/canary/translations/cs.json index 5b883f253be..1cc46650531 100644 --- a/homeassistant/components/canary/translations/cs.json +++ b/homeassistant/components/canary/translations/cs.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "Argumenty p\u0159edan\u00e9 ffmpeg pro kamery", "timeout": "\u010casov\u00fd limit po\u017eadavku (v sekund\u00e1ch)" } } diff --git a/homeassistant/components/cert_expiry/translations/cs.json b/homeassistant/components/cert_expiry/translations/cs.json index 61bd93f40b3..c4b61df7084 100644 --- a/homeassistant/components/cert_expiry/translations/cs.json +++ b/homeassistant/components/cert_expiry/translations/cs.json @@ -17,5 +17,6 @@ } } } - } + }, + "title": "Platnost certifik\u00e1tu" } \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/ca.json b/homeassistant/components/cloud/translations/ca.json new file mode 100644 index 00000000000..fede749c7dd --- /dev/null +++ b/homeassistant/components/cloud/translations/ca.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa activada", + "can_reach_cert_server": "Servidor de certificaci\u00f3 accessible", + "can_reach_cloud": "Home Assistant Cloud accessible", + "can_reach_cloud_auth": "Servidor d'autenticaci\u00f3 accessible", + "google_enabled": "Google activat", + "logged_in": "Sessi\u00f3 iniciada", + "relayer_connected": "Encaminador connectat", + "remote_connected": "Connexi\u00f3 remota establerta", + "remote_enabled": "Connexi\u00f3 remota activada", + "subscription_expiration": "Caducitat de la subscripci\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/lb.json b/homeassistant/components/cloud/translations/lb.json new file mode 100644 index 00000000000..3806c2d6ebe --- /dev/null +++ b/homeassistant/components/cloud/translations/lb.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa aktiv\u00e9iert", + "can_reach_cert_server": "Zertifikat Server ereechbar", + "can_reach_cloud": "Home Assistant Cloud ereechbar", + "can_reach_cloud_auth": "Authentifikatioun Server ereechbar", + "google_enabled": "Google aktiv\u00e9iert", + "logged_in": "Ageloggt", + "relayer_connected": "Relayer verbonnen", + "remote_connected": "Remote verbonnen", + "remote_enabled": "Remote aktiv\u00e9iert", + "subscription_expiration": "Abonnement Verfallsdatum" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/no.json b/homeassistant/components/cloud/translations/no.json index 96fddd84b2b..585811f0eb4 100644 --- a/homeassistant/components/cloud/translations/no.json +++ b/homeassistant/components/cloud/translations/no.json @@ -8,8 +8,8 @@ "google_enabled": "Google aktivert", "logged_in": "Logget inn", "relayer_connected": "Relayer tilkoblet", - "remote_connected": "Fjernkontroll tilkoblet", - "remote_enabled": "Fjernkontroll aktivert", + "remote_connected": "Ekstern tilkobling", + "remote_enabled": "Ekstern aktivert", "subscription_expiration": "Abonnementets utl\u00f8p" } } diff --git a/homeassistant/components/control4/translations/cs.json b/homeassistant/components/control4/translations/cs.json index c54454e9afe..327d642b308 100644 --- a/homeassistant/components/control4/translations/cs.json +++ b/homeassistant/components/control4/translations/cs.json @@ -14,6 +14,16 @@ "host": "IP adresa", "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "description": "Zadejte pros\u00edm \u00fadaje o sv\u00e9m \u00fa\u010dtu Control4 a IP adresu m\u00edstn\u00edho ovlada\u010de." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Po\u010det sekund mezi aktualizacemi" } } } diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index d5b5e066c37..3ad72dc9ac9 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "no_bridges": "\u017d\u00e1dn\u00e9 deCONZ p\u0159emost\u011bn\u00ed nebyly nalezeny", + "no_hardware_available": "K deCONZ nen\u00ed p\u0159ipojeno \u017e\u00e1dn\u00e9 r\u00e1diov\u00e9 za\u0159\u00edzen\u00ed", "not_deconz_bridge": "Nejedn\u00e1 se o deCONZ p\u0159emost\u011bn\u00ed", "updated_instance": "Instance deCONZ aktualizov\u00e1na s nov\u00fdm hostitelem" }, diff --git a/homeassistant/components/demo/translations/cs.json b/homeassistant/components/demo/translations/cs.json index 1a9e6570085..1bac6710266 100644 --- a/homeassistant/components/demo/translations/cs.json +++ b/homeassistant/components/demo/translations/cs.json @@ -3,6 +3,7 @@ "step": { "options_1": { "data": { + "constant": "Konstanta", "int": "\u010c\u00edseln\u00fd vstup" } }, diff --git a/homeassistant/components/denonavr/translations/cs.json b/homeassistant/components/denonavr/translations/cs.json index 0dd0a9db578..1c66dae10f0 100644 --- a/homeassistant/components/denonavr/translations/cs.json +++ b/homeassistant/components/denonavr/translations/cs.json @@ -3,9 +3,19 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "cannot_connect": "P\u0159ipojen\u00ed se nezda\u0159ilo, zkuste to pros\u00edm znovu - odpojen\u00ed napajec\u00edch a ethernetov\u00fdch kabel\u016f a jejich op\u011btovn\u00e9 p\u0159ipojen\u00ed m\u016f\u017ee pomoci" + "cannot_connect": "P\u0159ipojen\u00ed se nezda\u0159ilo, zkuste to pros\u00edm znovu - odpojen\u00ed napajec\u00edch a ethernetov\u00fdch kabel\u016f a jejich op\u011btovn\u00e9 p\u0159ipojen\u00ed m\u016f\u017ee pomoci", + "not_denonavr_manufacturer": "Nejedn\u00e1 se o s\u00ed\u0165ov\u00fd p\u0159ij\u00edma\u010d Denon AVR, objeven\u00fd v\u00fdrobce se neshoduje", + "not_denonavr_missing": "Nejedn\u00e1 se o s\u00ed\u0165ov\u00fd p\u0159ij\u00edma\u010d Denon AVR, informace o zji\u0161\u0165ov\u00e1n\u00ed nejsou \u00fapln\u00e9" }, + "error": { + "discovery_error": "Nepoda\u0159ilo se naj\u00edt s\u00ed\u0165ov\u00fd p\u0159ij\u00edma\u010d Denon AVR" + }, + "flow_title": "S\u00ed\u0165ov\u00fd p\u0159ij\u00edma\u010d Denon AVR: {name}", "step": { + "confirm": { + "description": "Potvr\u010fte pros\u00edm p\u0159id\u00e1n\u00ed p\u0159ij\u00edma\u010de", + "title": "S\u00ed\u0165ov\u00e9 p\u0159ij\u00edma\u010de Denon AVR" + }, "user": { "data": { "host": "IP adresa" diff --git a/homeassistant/components/doorbird/translations/cs.json b/homeassistant/components/doorbird/translations/cs.json index 6000c40e5a2..fea0647ec85 100644 --- a/homeassistant/components/doorbird/translations/cs.json +++ b/homeassistant/components/doorbird/translations/cs.json @@ -28,7 +28,8 @@ "init": { "data": { "events": "Seznam ud\u00e1lost\u00ed odd\u011blen\u00fdch \u010d\u00e1rkami." - } + }, + "description": "Zadejte n\u00e1zvy ud\u00e1lost\u00ed odd\u011blen\u00e9 \u010d\u00e1rkou, kter\u00e9 chcete sledovat. Po jejich zad\u00e1n\u00ed je pomoc\u00ed aplikace DoorBird p\u0159i\u0159a\u010fte ke konkr\u00e9tn\u00ed ud\u00e1losti. Viz dokumentace na https://www.home-assistant.io/integrations/doorbird/#events. P\u0159\u00edklad: nekdo_stiskl_tlacitko, pohyb" } } } diff --git a/homeassistant/components/dsmr/translations/ca.json b/homeassistant/components/dsmr/translations/ca.json index 14e637f5f98..a876776fea2 100644 --- a/homeassistant/components/dsmr/translations/ca.json +++ b/homeassistant/components/dsmr/translations/ca.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Temps m\u00ednim entre actualitzacions d'entitats [s]" + }, + "title": "Opcions de DSMR" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/cs.json b/homeassistant/components/dunehd/translations/cs.json index 231ff6055b6..b52eb3d2cdf 100644 --- a/homeassistant/components/dunehd/translations/cs.json +++ b/homeassistant/components/dunehd/translations/cs.json @@ -13,6 +13,7 @@ "data": { "host": "Hostitel" }, + "description": "Nastaven\u00ed integrace Dune HD. Pokud m\u00e1te probl\u00e9my s nastaven\u00edm, p\u0159ejd\u011bte na: https://www.home-assistant.io/integrations/dunehd \n\nUjist\u011bte se, \u017ee je v\u00e1\u0161 p\u0159ehr\u00e1va\u010d zapnut\u00fd.", "title": "Dune HD" } } diff --git a/homeassistant/components/flick_electric/translations/cs.json b/homeassistant/components/flick_electric/translations/cs.json index 9a09785d5d8..6653b76bcd2 100644 --- a/homeassistant/components/flick_electric/translations/cs.json +++ b/homeassistant/components/flick_electric/translations/cs.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "client_id": "ID klienta (voliteln\u00e9)", "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, diff --git a/homeassistant/components/forked_daapd/translations/cs.json b/homeassistant/components/forked_daapd/translations/cs.json index 8732191dab0..55fa46e68ca 100644 --- a/homeassistant/components/forked_daapd/translations/cs.json +++ b/homeassistant/components/forked_daapd/translations/cs.json @@ -7,16 +7,32 @@ "error": { "forbidden": "Nelze se p\u0159ipojit. Zkontrolujte pros\u00edm opr\u00e1vn\u011bn\u00ed s\u00edt\u011b forked-daapd.", "unknown_error": "Neo\u010dek\u00e1van\u00e1 chyba", + "websocket_not_enabled": "Websocket serveru forked-daapd nen\u00ed povolen.", + "wrong_host_or_port": "Nelze se p\u0159ipojit. Zkontrolujte hostitele a port.", "wrong_password": "Nespr\u00e1vn\u00e9 heslo.", "wrong_server_type": "Integrace forked-daapd vy\u017eaduje server forked-daapd s verz\u00ed >= 27.0." }, + "flow_title": "Server forked-daapd: {name} ({host})", "step": { "user": { "data": { "host": "Hostitel", "name": "Zobrazovan\u00e9 jm\u00e9no", - "password": "Heslo API (ponechte pr\u00e1zdn\u00e9, pokud \u017e\u00e1dn\u00e9 heslo nen\u00ed)" - } + "password": "Heslo API (ponechte pr\u00e1zdn\u00e9, pokud \u017e\u00e1dn\u00e9 heslo nen\u00ed)", + "port": "Port API" + }, + "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_playlists": "Maxim\u00e1ln\u00ed po\u010det seznam\u016f skladeb pou\u017eit\u00fdch jako zdroje" + }, + "description": "Nastavte r\u016fzn\u00e9 mo\u017enosti integrace forked-daapd.", + "title": "Nastavte mo\u017enosti forked-daapd" } } } diff --git a/homeassistant/components/goalzero/translations/cs.json b/homeassistant/components/goalzero/translations/cs.json index db33844a04d..4d39a29a7c3 100644 --- a/homeassistant/components/goalzero/translations/cs.json +++ b/homeassistant/components/goalzero/translations/cs.json @@ -14,6 +14,7 @@ "host": "Hostitel", "name": "Jm\u00e9no" }, + "description": "Nejprve si mus\u00edte st\u00e1hnout aplikaci Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nPodle pokyn\u016f p\u0159ipojte za\u0159\u00edzen\u00ed Yeti k s\u00edti Wifi. Pot\u00e9 z\u00edskejte hostitelskou IP z routeru. Aby se IP hostitele nezm\u011bnila, mus\u00ed b\u00fdt v nastaven\u00ed routeru pro za\u0159\u00edzen\u00ed nastaven DHCP. Informace nalezenete v u\u017eivatelsk\u00e9 p\u0159\u00edru\u010dce k routeru.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 981cb51c83a..ac804794b48 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "Placa", + "disk_total": "Total disc", + "disk_used": "Disc utilitzat", + "docker_version": "Versi\u00f3 de Docker", + "healthy": "Saludable", + "host_os": "Sistema operatiu amfitri\u00f3", + "installed_addons": "Complements instal\u00b7lats", + "supervisor_api": "API del Supervisor", + "supervisor_version": "Versi\u00f3 del Supervisor", + "supported": "Compatible", + "update_channel": "Canal d'actualitzaci\u00f3", + "version_api": "API de versions" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json index 981cb51c83a..729dc069d7d 100644 --- a/homeassistant/components/hassio/translations/cs.json +++ b/homeassistant/components/hassio/translations/cs.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "Deska", + "disk_total": "Kapacita disku", + "disk_used": "Obsazen\u00fd disk", + "docker_version": "Verze Dockeru", + "healthy": "V po\u0159\u00e1dku", + "host_os": "Hostitelsk\u00fd opera\u010dn\u00ed syst\u00e9m", + "installed_addons": "Nainstalovan\u00e9 dopl\u0148ky", + "supervisor_api": "API Supervisora", + "supervisor_version": "Verze Supervizora", + "supported": "Podporov\u00e1no", + "update_channel": "Kan\u00e1l aktualizac\u00ed", + "version_api": "Verze API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 875a79a60d7..230e0c11fea 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,18 +1,19 @@ { - "system_health": { - "info": { - "board": "Board", - "disk_total": "Disk Total", - "disk_used": "Disk Used", - "docker_version": "Docker Version", - "healthy": "Healthy", - "host_os": "Host Operating System", - "installed_addons": "Installed Add-ons", - "supervisor_api": "Supervisor API", - "supervisor_version": "Supervisor Version", - "supported": "Supported", - "update_channel": "Update Channel", - "version_api": "Version API" - } - } -} + "system_health": { + "info": { + "board": "Board", + "disk_total": "Disk Total", + "disk_used": "Disk Used", + "docker_version": "Docker Version", + "healthy": "Healthy", + "host_os": "Host Operating System", + "installed_addons": "Installed Add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor Version", + "supported": "Supported", + "update_channel": "Update Channel", + "version_api": "Version API" + } + }, + "title": "Hass.io" +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json new file mode 100644 index 00000000000..f02939b201e --- /dev/null +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -0,0 +1,20 @@ +{ + "system_health": { + "info": { + "arch": "Arquitectura de la CPU", + "chassis": "Xass\u00eds", + "dev": "Desenvolupament", + "docker": "Docker", + "docker_version": "Docker", + "host_os": "Home Assistant OS", + "installation_type": "Tipus d'instal\u00b7laci\u00f3", + "os_name": "Fam\u00edlia del sistema operatiu", + "os_version": "Versi\u00f3 del sistema operatiu", + "python_version": "Versi\u00f3 de Python", + "supervisor": "Supervisor", + "timezone": "Zona hor\u00e0ria", + "version": "Versi\u00f3", + "virtualenv": "Entorn virtual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/cs.json b/homeassistant/components/homeassistant/translations/cs.json index 59c99f1edfa..46bf2c56b4b 100644 --- a/homeassistant/components/homeassistant/translations/cs.json +++ b/homeassistant/components/homeassistant/translations/cs.json @@ -8,7 +8,7 @@ "docker_version": "Docker", "host_os": "Home Assistant OS", "installation_type": "Typ instalace", - "os_name": "Jm\u00e9no opera\u010dn\u00edho syst\u00e9mu", + "os_name": "Rodina opera\u010dn\u00edch syst\u00e9m\u016f", "os_version": "Verze opera\u010dn\u00edho syst\u00e9mu", "python_version": "Verze Pythonu", "supervisor": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 8e810ef2143..3eb1f91eefc 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,17 +1,20 @@ { - "system_health": { - "info": { - "arch": "CPU Architecture", - "dev": "Development", - "docker": "Docker", - "installation_type": "Installation Type", - "os_name": "Operating System Family", - "os_version": "Operating System Version", - "python_version": "Python Version", - "timezone": "Timezone", - "version": "Version", - "virtualenv": "Virtual Environment" + "system_health": { + "info": { + "arch": "CPU Architecture", + "chassis": "Chassis", + "dev": "Development", + "docker": "Docker", + "docker_version": "Docker", + "host_os": "Home Assistant OS", + "installation_type": "Installation Type", + "os_name": "Operating System Family", + "os_version": "Operating System Version", + "python_version": "Python Version", + "supervisor": "Supervisor", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" + } } - }, - "title": "Home Assistant" -} +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/cs.json b/homeassistant/components/homekit_controller/translations/cs.json index 6e41f423250..9a2159eda05 100644 --- a/homeassistant/components/homekit_controller/translations/cs.json +++ b/homeassistant/components/homekit_controller/translations/cs.json @@ -7,6 +7,7 @@ "already_paired": "Toto p\u0159\u00edslu\u0161enstv\u00ed je ji\u017e sp\u00e1rov\u00e1no s jin\u00fdm za\u0159\u00edzen\u00edm. Resetujte p\u0159\u00edslu\u0161enstv\u00ed a zkuste to znovu.", "ignored_model": "Podpora pro tento model je v HomeKit blokov\u00e1na, proto\u017ee je k dispozici nativn\u00ed integrace s v\u00edce funkcemi.", "invalid_config_entry": "Toto za\u0159\u00edzen\u00ed vypad\u00e1, \u017ee je p\u0159ipraven\u00e9 ke sp\u00e1rov\u00e1n\u00ed, ale v Home Assistant ji\u017e existuje konfliktn\u00ed polo\u017eka konfigurace, kterou je t\u0159eba nejprve odebrat.", + "invalid_properties": "Neplatn\u00e9 vlastnosti ozn\u00e1men\u00e9 za\u0159\u00edzen\u00edm.", "no_devices": "Nebyla nalezena \u017e\u00e1dn\u00e1 nesp\u00e1rov\u00e1 za\u0159\u00edzen\u00ed" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/cs.json b/homeassistant/components/huawei_lte/translations/cs.json index 79d8b594256..298cf182b08 100644 --- a/homeassistant/components/huawei_lte/translations/cs.json +++ b/homeassistant/components/huawei_lte/translations/cs.json @@ -23,6 +23,7 @@ "url": "URL", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, + "description": "Zadejte podrobnosti o p\u0159\u00edstupu k za\u0159\u00edzen\u00ed. Zad\u00e1n\u00ed u\u017eivatelsk\u00e9ho jm\u00e9na a hesla je voliteln\u00e9, ale umo\u017e\u0148uje podporu dal\u0161\u00edch funkc\u00ed integrace. Na druhou stranu m\u016f\u017ee pou\u017eit\u00ed autorizovan\u00e9ho p\u0159ipojen\u00ed zp\u016fsobit probl\u00e9my s p\u0159\u00edstupem k webov\u00e9mu rozhran\u00ed Home Assistant zven\u010d\u00ed, kdy\u017e je integrace aktivn\u00ed, a naopak.", "title": "Konfigurovat Huawei LTE" } } diff --git a/homeassistant/components/insteon/translations/cs.json b/homeassistant/components/insteon/translations/cs.json index 6b0effe3759..18d7ff1c999 100644 --- a/homeassistant/components/insteon/translations/cs.json +++ b/homeassistant/components/insteon/translations/cs.json @@ -30,7 +30,8 @@ "plm": { "data": { "device": "Cesta k USB za\u0159\u00edzen\u00ed" - } + }, + "description": "Nastavte modem Insteon PowerLink (PLM)." }, "user": { "data": { @@ -57,7 +58,10 @@ }, "add_x10": { "data": { - "platform": "Platforma" + "housecode": "K\u00f3d domu (a-p)", + "platform": "Platforma", + "steps": "Kroky stm\u00edva\u010de (pouze pro sv\u011btla, v\u00fdchoz\u00ed 22)", + "unitcode": "K\u00f3d jednotky (1-16)" }, "title": "Insteon" }, @@ -68,6 +72,7 @@ "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, + "description": "Zm\u011b\u0148te informace o p\u0159ipojen\u00ed rozbo\u010dova\u010de Insteon. Po proveden\u00ed t\u00e9to zm\u011bny mus\u00edte Home Assistant restartovat. T\u00edm se nezm\u011bn\u00ed nastaven\u00ed samotn\u00e9ho rozbo\u010dova\u010de. Chcete-li zm\u011bnit nastaven\u00ed rozbo\u010dova\u010de, pou\u017eijte jeho aplikaci.", "title": "Insteon" }, "init": { @@ -75,9 +80,13 @@ "add_x10": "P\u0159idejte za\u0159\u00edzen\u00ed X10.", "remove_x10": "Odeberte za\u0159\u00edzen\u00ed X10." }, + "description": "Vyberte mo\u017enost k nastaven\u00ed.", "title": "Insteon" }, "remove_override": { + "data": { + "address": "Vyberte adresu za\u0159\u00edzen\u00ed, kter\u00e9 chcete odebrat" + }, "title": "Insteon" }, "remove_x10": { diff --git a/homeassistant/components/iqvia/translations/cs.json b/homeassistant/components/iqvia/translations/cs.json index 6754cdc84af..04829af0b8f 100644 --- a/homeassistant/components/iqvia/translations/cs.json +++ b/homeassistant/components/iqvia/translations/cs.json @@ -11,6 +11,7 @@ "data": { "zip_code": "PS\u010c" }, + "description": "Vypl\u0148te sv\u00e9 americk\u00e9 nebo kanadsk\u00e9 PS\u010c.", "title": "IQVIA" } } diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index f5024d177fd..e2d3dc4c883 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_host": "Z\u00e1znam hostitele nebyl v \u00fapln\u00e9m form\u00e1tu URL, nap\u0159. http://192.168.10.100:80", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { @@ -13,8 +14,10 @@ "data": { "host": "URL", "password": "Heslo", + "tls": "Verze TLS ovlada\u010de ISY.", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, + "description": "Polo\u017eka hostitele mus\u00ed b\u00fdt v \u00fapln\u00e9m form\u00e1tu URL, nap\u0159 http://192.168.10.100:80.", "title": "P\u0159ipojen\u00ed k ISY994" } } diff --git a/homeassistant/components/kodi/translations/cs.json b/homeassistant/components/kodi/translations/cs.json index ca5a30dec36..e21c08b0758 100644 --- a/homeassistant/components/kodi/translations/cs.json +++ b/homeassistant/components/kodi/translations/cs.json @@ -29,7 +29,8 @@ "host": "Hostitel", "port": "Port", "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t" - } + }, + "description": "Informace o p\u0159ipojen\u00ed Kodi. Nezapome\u0148te povolit mo\u017enost \"Povolit ovl\u00e1d\u00e1n\u00ed Kodi prost\u0159ednictv\u00edm protokolu HTTP\" v Syst\u00e9m/Nastaven\u00ed/S\u00ed\u0165/Slu\u017eby." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/cs.json b/homeassistant/components/konnected/translations/cs.json index 8f575c34a51..f2cd8759a54 100644 --- a/homeassistant/components/konnected/translations/cs.json +++ b/homeassistant/components/konnected/translations/cs.json @@ -61,7 +61,8 @@ "5": "Z\u00f3na 5", "6": "Z\u00f3na 6", "7": "Z\u00f3na 7" - } + }, + "title": "Nastaven\u00ed vstupu/v\u00fdstupu" }, "options_io_ext": { "data": { diff --git a/homeassistant/components/lovelace/translations/ca.json b/homeassistant/components/lovelace/translations/ca.json new file mode 100644 index 00000000000..65408175090 --- /dev/null +++ b/homeassistant/components/lovelace/translations/ca.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "dashboards": "Panells", + "mode": "Mode", + "resources": "Recursos" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/cs.json b/homeassistant/components/netatmo/translations/cs.json index 74a31f4199d..7857e345165 100644 --- a/homeassistant/components/netatmo/translations/cs.json +++ b/homeassistant/components/netatmo/translations/cs.json @@ -26,13 +26,17 @@ "lon_sw": "Zem\u011bpisn\u00e1 d\u00e9lka Jihoz\u00e1padn\u00ed roh", "mode": "V\u00fdpo\u010det", "show_on_map": "Zobrazit na map\u011b" - } + }, + "description": "Nastavte ve\u0159ejn\u00fd senzor po\u010das\u00ed pro oblast.", + "title": "Ve\u0159ejn\u00fd senzor po\u010das\u00ed Netatmo" }, "public_weather_areas": { "data": { "new_area": "Jm\u00e9no oblasti", "weather_areas": "Oblasti po\u010das\u00ed" - } + }, + "description": "Nastavte ve\u0159ejn\u00e9 senzory po\u010das\u00ed.", + "title": "Ve\u0159ejn\u00fd senzor po\u010das\u00ed Netatmo" } } } diff --git a/homeassistant/components/plex/translations/cs.json b/homeassistant/components/plex/translations/cs.json index bef1271888f..de85391a7d9 100644 --- a/homeassistant/components/plex/translations/cs.json +++ b/homeassistant/components/plex/translations/cs.json @@ -37,6 +37,9 @@ "title": "Plex Media Server" }, "user_advanced": { + "data": { + "setup_method": "Metoda nastaven\u00ed" + }, "title": "Plex Media Server" } } diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json index 3ad7fad2a6b..c1c193e04a2 100644 --- a/homeassistant/components/plugwise/translations/cs.json +++ b/homeassistant/components/plugwise/translations/cs.json @@ -8,6 +8,7 @@ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { @@ -19,6 +20,7 @@ "user_gateway": { "data": { "host": "IP adresa", + "password": "Smile ID", "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no Smile" }, diff --git a/homeassistant/components/rfxtrx/translations/cs.json b/homeassistant/components/rfxtrx/translations/cs.json index 5052e35fd1e..706d4499fb2 100644 --- a/homeassistant/components/rfxtrx/translations/cs.json +++ b/homeassistant/components/rfxtrx/translations/cs.json @@ -41,6 +41,7 @@ "invalid_event_code": "Neplatn\u00fd k\u00f3d ud\u00e1losti", "invalid_input_2262_off": "Neplatn\u00fd vstup pro vyp\u00ednac\u00ed p\u0159\u00edkaz", "invalid_input_2262_on": "Neplatn\u00fd vstup pro zap\u00ednac\u00ed p\u0159\u00edkaz", + "invalid_input_off_delay": "Neplatn\u00e1 hodnota pro zpo\u017ed\u011bn\u00ed vypnut\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { @@ -49,13 +50,18 @@ "automatic_add": "Povolit automatick\u00e9 p\u0159id\u00e1n\u00ed", "debug": "Povolit lad\u011bn\u00ed", "device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit", + "event_code": "Zadejte k\u00f3d ud\u00e1losti, kterou chcete p\u0159idat", "remove_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete odstranit" }, "title": "Mo\u017enosti Rfxtrx" }, "set_device_options": { "data": { - "replace_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete vym\u011bnit" + "fire_event": "Povolit ud\u00e1lost za\u0159\u00edzen\u00ed", + "off_delay": "Zpo\u017ed\u011bn\u00ed vypnut\u00ed", + "off_delay_enabled": "Povolit zpo\u017ed\u011bn\u00ed vypnut\u00ed", + "replace_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete vym\u011bnit", + "signal_repetitions": "Po\u010det opakov\u00e1n\u00ed sign\u00e1lu" }, "title": "Nastaven\u00ed mo\u017enost\u00ed za\u0159\u00edzen\u00ed" } diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index 9418fa39cbb..a15e75066a9 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -10,6 +10,7 @@ }, "step": { "link": { + "description": "Mus\u00edte povolit Home Assistant v Roon. Po kliknut\u00ed na Odeslat p\u0159ejd\u011bte do aplikace Roon Core, otev\u0159ete Nastaven\u00ed a na z\u00e1lo\u017ece Roz\u0161\u00ed\u0159en\u00ed povolte Home Assistant.", "title": "Autorizujte HomeAssistant v Roon" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/cs.json b/homeassistant/components/simplisafe/translations/cs.json index 41487b69d78..7fd81a94391 100644 --- a/homeassistant/components/simplisafe/translations/cs.json +++ b/homeassistant/components/simplisafe/translations/cs.json @@ -11,12 +11,14 @@ }, "step": { "mfa": { + "description": "Zkontrolujte, zda v\u00e1m do e-mail p\u0159i\u0161el odkaz od SimpliSafe. Po ov\u011b\u0159en\u00ed odkazu se vra\u0165te sem a dokon\u010dete instalaci integrace.", "title": "V\u00edcefaktorov\u00e9 ov\u011b\u0159ov\u00e1n\u00ed SimpliSafe" }, "reauth_confirm": { "data": { "password": "Heslo" }, + "description": "Platnost va\u0161eho p\u0159\u00edstupov\u00e9ho tokenu vypr\u0161ela nebo byla zru\u0161ena. Chcete-li sv\u016fj \u00fa\u010det znovu propojit, zadejte sv\u00e9 heslo.", "title": "Znovu ov\u011b\u0159it integraci" }, "user": { diff --git a/homeassistant/components/smappee/translations/cs.json b/homeassistant/components/smappee/translations/cs.json index 015d2fda130..174da41205a 100644 --- a/homeassistant/components/smappee/translations/cs.json +++ b/homeassistant/components/smappee/translations/cs.json @@ -2,8 +2,10 @@ "config": { "abort": { "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_configured_local_device": "M\u00edstn\u00ed za\u0159\u00edzen\u00ed ji\u017e jsou nastavena. P\u0159ed nastaven\u00ed cloudov\u00e9ho za\u0159\u00edzen\u00ed je nejprve odstra\u0148te.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_mdns": "Nepodporovan\u00e9 za\u0159\u00edzen\u00ed pro integraci Smappee.", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" }, diff --git a/homeassistant/components/smarthab/translations/cs.json b/homeassistant/components/smarthab/translations/cs.json index bb13d19ad93..1e862ff0069 100644 --- a/homeassistant/components/smarthab/translations/cs.json +++ b/homeassistant/components/smarthab/translations/cs.json @@ -9,7 +9,8 @@ "data": { "email": "E-mail", "password": "Heslo" - } + }, + "title": "Nastaven\u00ed SmartHab" } } } diff --git a/homeassistant/components/smartthings/translations/cs.json b/homeassistant/components/smartthings/translations/cs.json index a634bcceb0e..a9279722712 100644 --- a/homeassistant/components/smartthings/translations/cs.json +++ b/homeassistant/components/smartthings/translations/cs.json @@ -19,12 +19,14 @@ "data": { "access_token": "P\u0159\u00edstupov\u00fd token" }, + "description": "Zadejte [osobn\u00ed p\u0159\u00edstupov\u00fd token]({token_url}) SmartThings, kter\u00fd byl vytvo\u0159en podle [pokyn\u016f]({component_url}). Ten se pou\u017eije k vytvo\u0159en\u00ed integrace Home Assistant ve va\u0161em \u00fa\u010dtu SmartThings.", "title": "Zadejte osobn\u00ed p\u0159\u00edstupov\u00fd token" }, "select_location": { "data": { "location_id": "Um\u00edst\u011bn\u00ed" }, + "description": "Vyberte um\u00edst\u011bn\u00ed SmartThings, kter\u00e9 chcete p\u0159idat do Home Assistant. Pot\u00e9 otev\u0159eme nov\u00e9 okno a po\u017e\u00e1d\u00e1me v\u00e1s o p\u0159ihl\u00e1\u0161en\u00ed a autorizaci instalace integrace Home Assistant do vybran\u00e9ho um\u00edst\u011bn\u00ed.", "title": "Vyberte um\u00edst\u011bn\u00ed" }, "user": { diff --git a/homeassistant/components/soma/translations/cs.json b/homeassistant/components/soma/translations/cs.json index eeee574cf20..5a27562df71 100644 --- a/homeassistant/components/soma/translations/cs.json +++ b/homeassistant/components/soma/translations/cs.json @@ -4,7 +4,8 @@ "already_setup": "M\u016f\u017eete nastavit pouze jeden \u00fa\u010det Soma.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "connection_error": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed SOMA Connect se nezda\u0159ilo.", - "missing_configuration": "Integrace Soma nen\u00ed nastavena. Postupujte podle dokumentace." + "missing_configuration": "Integrace Soma nen\u00ed nastavena. Postupujte podle dokumentace.", + "result_error": "SOMA Connect odpov\u011bd\u011blo chybov\u00fdm stavem." }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno pomoc\u00ed Soma." diff --git a/homeassistant/components/syncthru/translations/cs.json b/homeassistant/components/syncthru/translations/cs.json index 7527e6d81c8..d34668146a3 100644 --- a/homeassistant/components/syncthru/translations/cs.json +++ b/homeassistant/components/syncthru/translations/cs.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, + "error": { + "invalid_url": "Neplatn\u00e1 URL adresa" + }, "flow_title": "Tisk\u00e1rna Samsung SyncThru: {name}", "step": { "confirm": { diff --git a/homeassistant/components/toon/translations/cs.json b/homeassistant/components/toon/translations/cs.json index 3039fa9f061..52a2f7b5742 100644 --- a/homeassistant/components/toon/translations/cs.json +++ b/homeassistant/components/toon/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" diff --git a/homeassistant/components/transmission/translations/cs.json b/homeassistant/components/transmission/translations/cs.json index 73995f2eece..8ad9e051064 100644 --- a/homeassistant/components/transmission/translations/cs.json +++ b/homeassistant/components/transmission/translations/cs.json @@ -25,6 +25,7 @@ "init": { "data": { "limit": "Limit", + "order": "Po\u0159ad\u00ed", "scan_interval": "Frekvence aktualizac\u00ed" }, "title": "Nakonfigurujte mo\u017enosti pro Transmission" diff --git a/homeassistant/components/vera/translations/cs.json b/homeassistant/components/vera/translations/cs.json index c1cf289ae08..df93575a945 100644 --- a/homeassistant/components/vera/translations/cs.json +++ b/homeassistant/components/vera/translations/cs.json @@ -22,6 +22,7 @@ "exclude": "ID za\u0159\u00edzen\u00ed Vera, kter\u00e1 chcete vylou\u010dit z Home Assistant.", "lights": "ID za\u0159\u00edzen\u00ed Vera, se kter\u00fdmi m\u00e1 Home Assistant zach\u00e1zet jako se sv\u011btly." }, + "description": "Podrobnosti o voliteln\u00fdch parametrech najdete v dokumentaci vera: https://www.home-assistant.io/integrations/vera/. Pozn\u00e1mka: Jak\u00e9koli zm\u011bny zde budou vy\u017eadovat restart serveru Home Assistant. Chcete-li vymazat hodnoty, zadejte mezeru.", "title": "Mo\u017enosti ovlada\u010de Vera" } } diff --git a/homeassistant/components/vilfo/translations/cs.json b/homeassistant/components/vilfo/translations/cs.json index 8b423a73518..b6735bd64cb 100644 --- a/homeassistant/components/vilfo/translations/cs.json +++ b/homeassistant/components/vilfo/translations/cs.json @@ -14,6 +14,7 @@ "access_token": "P\u0159\u00edstupov\u00fd token", "host": "Hostitel" }, + "description": "Nastaven\u00ed integrace routeru Vilfo. Pot\u0159ebujete n\u00e1zev hostitele/IP adresu routeru Vilfo a p\u0159\u00edstupov\u00fd API token. Dal\u0161\u00ed informace o t\u00e9to integraci a o tom, jak tyto podrobnosti z\u00edskat, najdete na adrese: https://www.home-assistant.io/integrations/vilfo", "title": "P\u0159ipojen\u00ed k routeru Vilfo" } } diff --git a/homeassistant/components/vizio/translations/cs.json b/homeassistant/components/vizio/translations/cs.json index 963461737bb..23fec08499b 100644 --- a/homeassistant/components/vizio/translations/cs.json +++ b/homeassistant/components/vizio/translations/cs.json @@ -5,7 +5,8 @@ "updated_entry": "Tato polo\u017eka ji\u017e byla nastavena, ale jm\u00e9no, aplikace nebo mo\u017enosti definovan\u00e9 v konfiguraci neodpov\u00eddaj\u00ed d\u0159\u00edve importovan\u00e9 konfiguraci, tak\u017ee polo\u017eka konfigurace byla odpov\u00eddaj\u00edc\u00edm zp\u016fsobem aktualizov\u00e1na." }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "existing_config_entry_found": "Za\u0159\u00edzen\u00ed VIZIO SmartCast se stejn\u00fdm s\u00e9riov\u00fdm \u010d\u00edslem ji\u017e bylo nastaveno. Chcete-li nastavit tuto polo\u017eku, mus\u00edte odstranit st\u00e1vaj\u00edc\u00ed polo\u017eku." }, "step": { "pair_tv": { diff --git a/homeassistant/components/wolflink/translations/cs.json b/homeassistant/components/wolflink/translations/cs.json index b66ca57cd44..e532b833851 100644 --- a/homeassistant/components/wolflink/translations/cs.json +++ b/homeassistant/components/wolflink/translations/cs.json @@ -12,7 +12,8 @@ "device": { "data": { "device_name": "Za\u0159\u00edzen\u00ed" - } + }, + "title": "Vyberte za\u0159\u00edzen\u00ed WOLF" }, "user": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.cs.json b/homeassistant/components/wolflink/translations/sensor.cs.json index a295a04fb34..046fc4e6ed9 100644 --- a/homeassistant/components/wolflink/translations/sensor.cs.json +++ b/homeassistant/components/wolflink/translations/sensor.cs.json @@ -1,13 +1,27 @@ { "state": { "wolflink__state": { + "aktiviert": "Aktivov\u00e1no", + "aus": "Zak\u00e1z\u00e1no", "auto": "Automatika", + "automatik_aus": "Automatick\u00e9 vypnut\u00ed", + "automatik_ein": "Automatick\u00e9 zapnut\u00ed", "cooling": "Chlazen\u00ed", + "deaktiviert": "Neaktivn\u00ed", "eco": "Ekonomick\u00fd re\u017eim", "ein": "Povoleno", + "externe_deaktivierung": "Extern\u00ed deaktivace", + "frostschutz": "Ochrana proti mrazu", + "gasdruck": "Tlak plynu", "heizbetrieb": "Re\u017eim topen\u00ed", "heizung": "Topen\u00ed", + "initialisierung": "Inicializace", + "kalibration": "Kalibrace", "permanent": "Trval\u00fd", + "schornsteinfeger": "Zkou\u0161ka emis\u00ed", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", + "softstart": "M\u011bkk\u00fd start", "sparbetrieb": "Ekonomick\u00fd re\u017eim", "sparen": "Ekonomick\u00fd re\u017eim", "standby": "Pohotovostn\u00ed re\u017eim", diff --git a/homeassistant/components/xiaomi_aqara/translations/cs.json b/homeassistant/components/xiaomi_aqara/translations/cs.json index 2f8110f22e4..b344be8c647 100644 --- a/homeassistant/components/xiaomi_aqara/translations/cs.json +++ b/homeassistant/components/xiaomi_aqara/translations/cs.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "not_xiaomi_aqara": "Za\u0159\u00edzen\u00ed nen\u00ed br\u00e1na Xiaomi Aqara, objeven\u00e9 za\u0159\u00edzen\u00ed neodpov\u00edd\u00e1 zn\u00e1m\u00fdm bran\u00e1m" }, "error": { + "discovery_error": "Nepoda\u0159ilo se naj\u00edt br\u00e1nu Xiaomi Aqara, zkuste jako rozhran\u00ed pou\u017e\u00edt IP adresu za\u0159\u00edzen\u00ed, na kter\u00e9m je spu\u0161t\u011bn HomeAssistant.", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa , viz https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Neplatn\u00e9 s\u00ed\u0165ov\u00e9 rozhran\u00ed", "invalid_key": "Neplatn\u00fd kl\u00ed\u010d br\u00e1ny", @@ -24,6 +26,7 @@ "key": "Kl\u00ed\u010d va\u0161\u00ed br\u00e1ny", "name": "Jm\u00e9no br\u00e1ny" }, + "description": "Kl\u00ed\u010d (heslo) lze z\u00edskat pomoc\u00ed tohoto n\u00e1vodu: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Pokud kl\u00ed\u010d nen\u00ed k dispozici, budou p\u0159\u00edstupn\u00e9 pouze senzory", "title": "Br\u00e1na Xiaomi Aqara, voliteln\u00e1 nastaven\u00ed" }, "user": { diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 119789e841e..1ac4c7c2d61 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -14,6 +14,7 @@ "title": "Nastaven\u00ed" }, "user": { + "description": "Vyberte s\u00e9riov\u00fd port pro r\u00e1dio Zigbee", "title": "ZHA" } } From be93060e993ce417cf8309c42d0cc86e804fbc75 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 12 Nov 2020 00:37:27 -0500 Subject: [PATCH 010/430] Move setup_url_for_address to pyWeMo library (#42722) * Move setup_url_for_address to PyWemo library * Bump pywemo to 0.5.2 * Use module-level function call * Update requirements via script --- homeassistant/components/wemo/__init__.py | 13 +------------ homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 0594747d0b2..c656926ecff 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass, entry): def validate_static_config(host, port): """Handle a static config.""" - url = setup_url_for_address(host, port) + url = pywemo.setup_url_for_address(host, port) if not url: _LOGGER.error( @@ -183,14 +183,3 @@ def validate_static_config(host, port): return None return device - - -def setup_url_for_address(host, port): - """Determine setup.xml url for given host and port pair.""" - if not port: - port = pywemo.ouimeaux_device.probe_wemo(host) - - if not port: - return None - - return f"http://{host}:{port}/setup.xml" diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 357d9d95483..a7d2ca585a5 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.5.0"], + "requirements": ["pywemo==0.5.2"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index d910f50a3e8..c06edfd25cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1891,7 +1891,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.0 +pywemo==0.5.2 # homeassistant.components.wilight pywilight==0.0.65 From 22a0464dce3ad4192d82236d30856dedf95761df Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 12 Nov 2020 11:12:56 +0200 Subject: [PATCH 011/430] Make Appliance Type Case-insensitive (#43114) "appliance_type" is a free text parameter in the device settings, this fix will make the comparison case-insensitive --- homeassistant/components/shelly/light.py | 20 +++++++++--------- homeassistant/components/shelly/switch.py | 25 +++++++++++++---------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index b62b5ee5e8c..83c7f3cf177 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -30,18 +30,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for block in wrapper.device.blocks: if block.type == "light": blocks.append(block) - elif ( - block.type == "relay" - and wrapper.device.settings["relays"][int(block.channel)].get( + elif block.type == "relay": + appliance_type = wrapper.device.settings["relays"][int(block.channel)].get( "appliance_type" ) - == "light" - ): - blocks.append(block) - unique_id = f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' - await async_remove_entity_by_domain( - hass, "switch", unique_id, config_entry.entry_id - ) + if appliance_type and appliance_type.lower() == "light": + blocks.append(block) + unique_id = ( + f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' + ) + await async_remove_entity_by_domain( + hass, "switch", unique_id, config_entry.entry_id + ) if not blocks: return diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index ee5ecc5d8f8..653f090bf4e 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -23,18 +23,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): relay_blocks = [] for block in wrapper.device.blocks: - if block.type == "relay" and ( - wrapper.device.settings["relays"][int(block.channel)].get("appliance_type") - != "light" - ): - relay_blocks.append(block) - unique_id = f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' - await async_remove_entity_by_domain( - hass, - "light", - unique_id, - config_entry.entry_id, + if block.type == "relay": + appliance_type = wrapper.device.settings["relays"][int(block.channel)].get( + "appliance_type" ) + if not appliance_type or appliance_type.lower() != "light": + relay_blocks.append(block) + unique_id = ( + f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' + ) + await async_remove_entity_by_domain( + hass, + "light", + unique_id, + config_entry.entry_id, + ) if not relay_blocks: return From d7e5d1bfafbef7a87c453d06e25531c1bfeeb96c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 12 Nov 2020 10:33:01 +0100 Subject: [PATCH 012/430] Bump dependency for HomematicIP Cloud (#43018) Co-authored-by: Paulus Schoutsen --- .../components/homematicip_cloud/__init__.py | 36 ++- .../homematicip_cloud/binary_sensor.py | 10 +- .../components/homematicip_cloud/hap.py | 5 +- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/sensor.py | 40 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homematicip_cloud/conftest.py | 2 +- tests/components/homematicip_cloud/helper.py | 6 +- .../homematicip_cloud/test_binary_sensor.py | 10 +- .../homematicip_cloud/test_device.py | 4 +- .../components/homematicip_cloud/test_init.py | 5 +- .../homematicip_cloud/test_sensor.py | 13 +- tests/fixtures/homematicip_cloud.json | 249 +++++++++++++++++- 14 files changed, 320 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 2b078966ee8..7ea6a4fe0b4 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,11 +1,14 @@ """Support for HomematicIP Cloud devices.""" +import logging + import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( @@ -20,6 +23,8 @@ from .generic_entity import HomematicipGenericEntity # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 from .services import async_setup_services, async_unload_services +_LOGGER = logging.getLogger(__name__) + CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=[]): vol.All( @@ -83,6 +88,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return False await async_setup_services(hass) + await async_remove_obsolete_entities(hass, entry, hap) # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection hap.reset_connection_listener = hass.bus.async_listen_once( @@ -91,16 +97,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool # Register hap as device in registry. 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.name} {home.label}" + hapname = home.label if home.label != entry.unique_id else f"Home-{home.label}" + device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, home.id)}, manufacturer="eQ-3", + # Add the name from config entry. name=hapname, - model=home.modelType, - sw_version=home.currentAPVersion, ) return True @@ -113,3 +119,23 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo await async_unload_services(hass) return await hap.async_reset() + + +async def async_remove_obsolete_entities( + hass: HomeAssistantType, entry: ConfigEntry, hap: HomematicipHAP +): + """Remove obsolete entities from entity registry.""" + + if hap.home.currentAPVersion < "2.2.12": + return + + entity_registry = await er.async_get_registry(hass) + er_entries = async_entries_for_config_entry(entity_registry, entry.entry_id) + for er_entry in er_entries: + if er_entry.unique_id.startswith("HomematicipAccesspointStatus"): + entity_registry.async_remove(er_entry.entity_id) + continue + + for hapid in hap.home.accessPointUpdateStates: + if er_entry.unique_id == f"HomematicipBatterySensor_{hapid}": + entity_registry.async_remove(er_entry.entity_id) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index e03738e4a76..12bca8378c3 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -146,7 +146,15 @@ class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEnt def __init__(self, hap: HomematicipHAP) -> None: """Initialize the cloud connection sensor.""" - super().__init__(hap, hap.home, "Cloud Connection") + super().__init__(hap, hap.home) + + @property + def name(self) -> str: + """Return the name cloud connection entity.""" + + name = "Cloud Connection" + # Add a prefix to the name if the homematic ip home has a name. + return name if not self._home.name else f"{self._home.name} {name}" @property def device_info(self) -> Dict[str, Any]: diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 164997d5582..151807391b9 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -240,8 +240,9 @@ class HomematicipHAP: home = AsyncHome(hass.loop, async_get_clientsession(hass)) home.name = name - home.label = "Access Point" - home.modelType = "HmIP-HAP" + # Use the title of the config entry as title for the home. + home.label = self.config_entry.title + home.modelType = "HomematicIP Cloud Home" home.set_auth_token(authtoken) try: diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 3ecf668e449..30ca5165c85 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.11.0"], + "requirements": ["homematicip==0.12.1"], "codeowners": ["@SukramJ"], "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 66e0e1afd70..9e202302c10 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -6,6 +6,7 @@ from homematicip.aio.device import ( AsyncFullFlushSwitchMeasuring, AsyncHeatingThermostat, AsyncHeatingThermostatCompact, + AsyncHomeControlAccessPoint, AsyncLightSensor, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, @@ -39,7 +40,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity -from .generic_entity import ATTR_IS_GROUP, ATTR_MODEL_TYPE from .hap import HomematicipHAP ATTR_CURRENT_ILLUMINATION = "current_illumination" @@ -63,8 +63,10 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - entities = [HomematicipAccesspointStatus(hap)] + entities = [] for device in hap.home.devices: + if isinstance(device, AsyncHomeControlAccessPoint): + entities.append(HomematicipAccesspointDutyCycle(hap, device)) if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): entities.append(HomematicipHeatingThermostat(hap, device)) entities.append(HomematicipTemperatureSensor(hap, device)) @@ -119,23 +121,12 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipAccesspointStatus(HomematicipGenericEntity): +class HomematicipAccesspointDutyCycle(HomematicipGenericEntity): """Representation of then HomeMaticIP access point.""" - def __init__(self, hap: HomematicipHAP) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize access point status entity.""" - super().__init__(hap, device=hap.home, post="Duty Cycle") - - @property - def device_info(self) -> Dict[str, Any]: - """Return device specific attributes.""" - # Adds a sensor to the existing HAP device - return { - "identifiers": { - # Serial numbers of Homematic IP device - (HMIPC_DOMAIN, self._home.id) - } - } + super().__init__(hap, device, post="Duty Cycle") @property def icon(self) -> str: @@ -145,28 +136,13 @@ class HomematicipAccesspointStatus(HomematicipGenericEntity): @property def state(self) -> float: """Return the state of the access point.""" - return self._home.dutyCycle - - @property - def available(self) -> bool: - """Return if access point is available.""" - return self._home.connected + return self._device.dutyCycleLevel @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the state attributes of the access point.""" - state_attr = super().device_state_attributes - - state_attr[ATTR_MODEL_TYPE] = "HmIP-HAP" - state_attr[ATTR_IS_GROUP] = False - - return state_attr - class HomematicipHeatingThermostat(HomematicipGenericEntity): """Representation of the HomematicIP heating thermostat.""" diff --git a/requirements_all.txt b/requirements_all.txt index c06edfd25cc..d30c5b463aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.11.0 +homematicip==0.12.1 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c00bb2bbc40..8d1887a5ce3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -406,7 +406,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.11.0 +homematicip==0.12.1 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b967ef79094..9764ee74e22 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -56,7 +56,7 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: config_entry = MockConfigEntry( version=1, domain=HMIPC_DOMAIN, - title=HAPID, + title="Home Test SN", unique_id=HAPID, data=entry_data, source=SOURCE_IMPORT, diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index dbbf0249c0c..ca7d8862756 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -136,9 +136,9 @@ class HomeTemplate(Home): def __init__(self, connection=None, home_name="", test_devices=[], test_groups=[]): """Init template with connection.""" super().__init__(connection=connection) - self.label = "Access Point" self.name = home_name - self.model_type = "HmIP-HAP" + self.label = "Home" + self.model_type = "HomematicIP Home" self.init_json_state = None self.test_devices = test_devices self.test_groups = test_groups @@ -196,7 +196,7 @@ class HomeTemplate(Home): and sets required attributes. """ mock_home = Mock( - spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP" + spec=AsyncHome, wraps=self, label="Home", modelType="HomematicIP Home" ) mock_home.__dict__.update(self.__dict__) diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index f7bfcbf2f96..420977cd40c 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -38,12 +38,10 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_access_point_cloud_connection_sensor( - hass, default_mock_hap_factory -): +async def test_hmip_home_cloud_connection_sensor(hass, default_mock_hap_factory): """Test HomematicipCloudConnectionSensor.""" - entity_id = "binary_sensor.access_point_cloud_connection" - entity_name = "Access Point Cloud Connection" + entity_id = "binary_sensor.cloud_connection" + entity_name = "Cloud Connection" device_model = None mock_hap = await default_mock_hap_factory.async_get_mock_hap( test_devices=[entity_name] @@ -55,7 +53,7 @@ async def test_hmip_access_point_cloud_connection_sensor( assert ha_state.state == STATE_ON - await async_manipulate_test_data(hass, hmip_device, "connected", False) + await async_manipulate_test_data(hass, mock_hap.home, "connected", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 4fecd3865a4..31e62a1a719 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 231 + assert len(mock_hap.hmip_device_by_entity_id) == 233 async def test_hmip_remove_device(hass, default_mock_hap_factory): @@ -268,4 +268,4 @@ async def test_hmip_multi_area_device(hass, default_mock_hap_factory): # get the hap hap_device = device_registry.async_get(device.via_device_id) - assert hap_device.name == "Access Point" + assert hap_device.name == "Home" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 37b293a3452..46059f12d00 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -145,6 +145,7 @@ async def test_unload_entry(hass): instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" + instance.home.label = "mock-label" instance.home.currentAPVersion = "mock-ap-version" instance.async_reset = AsyncMock(return_value=True) @@ -158,7 +159,7 @@ async def test_unload_entry(hass): assert config_entries[0].state == ENTRY_STATE_LOADED await hass.config_entries.async_unload(config_entries[0].entry_id) assert config_entries[0].state == ENTRY_STATE_NOT_LOADED - assert mock_hap.return_value.mock_calls[3][0] == "async_reset" + assert mock_hap.return_value.mock_calls[2][0] == "async_reset" # entry is unloaded assert hass.data[HMIPC_DOMAIN] == {} @@ -187,6 +188,7 @@ async def test_setup_services_and_unload_services(hass): instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" + instance.home.label = "mock-label" instance.home.currentAPVersion = "mock-ap-version" instance.async_reset = AsyncMock(return_value=True) @@ -220,6 +222,7 @@ async def test_setup_two_haps_unload_one_by_one(hass): instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" + instance.home.label = "mock-label" instance.home.currentAPVersion = "mock-ap-version" instance.async_reset = AsyncMock(return_value=True) diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 20c5c41a5b5..34c119595b3 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -46,11 +46,11 @@ async def test_manually_configured_platform(hass): async def test_hmip_accesspoint_status(hass, default_mock_hap_factory): """Test HomematicipSwitch.""" - entity_id = "sensor.access_point_duty_cycle" - entity_name = "Access Point Duty Cycle" - device_model = None + entity_id = "sensor.home_control_access_point_duty_cycle" + entity_name = "HOME_CONTROL_ACCESS_POINT Duty Cycle" + device_model = "HmIP-HAP" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=[entity_name] + test_devices=["HOME_CONTROL_ACCESS_POINT"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -60,11 +60,6 @@ async def test_hmip_accesspoint_status(hass, default_mock_hap_factory): assert ha_state.state == "8.0" assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - 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_factory): """Test HomematicipHeatingThermostat.""" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 4060ca7a820..9c2a1b1e371 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -14,6 +14,248 @@ } }, "devices": { + "3014F711A000000BAD0CAAAA": { + "availableFirmwareVersion": "2.2.18", + "connectionType": "HMIP_LAN", + "firmwareVersion": "2.2.18", + "firmwareVersionInteger": 131602, + "functionalChannels": { + "0": { + "accessPointPriority": 0, + "busConfigMismatch": null, + "carrierSenseLevel": 2.0, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F711A000000BAD0CAAAA", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "dutyCycleLevel": 8.0, + "functionalChannelType": "ACCESS_CONTROLLER_CHANNEL", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "signalBrightness": 1.0, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": false, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711A000000BAD0CAAAA", + "label": "AP1", + "lastStatusUpdate": 1604522238580, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 270, + "modelType": "HmIP-HAP", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711A000000BAD0CAAAA", + "type": "HOME_CONTROL_ACCESS_POINT", + "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED" + }, + "3014F711A000000BAD0C0DED": { + "availableFirmwareVersion": "2.2.18", + "connectionType": "HMIP_LAN", + "firmwareVersion": "2.2.18", + "firmwareVersionInteger": 131602, + "functionalChannels": { + "0": { + "accessPointPriority": 1, + "busConfigMismatch": null, + "carrierSenseLevel": 2.0, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F711A000000BAD0C0DED", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "dutyCycleLevel": 8.0, + "functionalChannelType": "ACCESS_CONTROLLER_CHANNEL", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "signalBrightness": 1.0, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": false, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711A000000BAD0C0DED", + "label": "HOME_CONTROL_ACCESS_POINT", + "lastStatusUpdate": 1604522238580, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 270, + "modelType": "HmIP-HAP", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711A000000BAD0C0DED", + "type": "HOME_CONTROL_ACCESS_POINT", + "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED" + }, + "3014F71100BLIND_MODULE00": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.4", + "firmwareVersionInteger": 65540, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F71100BLIND_MODULE00", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -85, + "rssiPeerValue": -78, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "automationDriveSpeed": "SLOW_SPEED", + "deviceId": "3014F71100BLIND_MODULE00", + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "SHADING_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "identifyOemSupported": true, + "index": 1, + "label": "", + "manualDriveSpeed": "NOMINAL_SPEED", + "previousPrimaryShadingLevel": null, + "previousSecondaryShadingLevel": null, + "primaryCloseAdjustable": true, + "primaryOpenAdjustable": true, + "primaryShadingLevel": 0.94956, + "primaryShadingStateType": "POSITION_USED", + "processing": false, + "productId": 10, + "profileMode": "AUTOMATIC", + "secondaryCloseAdjustable": false, + "secondaryOpenAdjustable": false, + "secondaryShadingLevel": null, + "secondaryShadingStateType": "NOT_EXISTENT", + "shadingDriveVersion": null, + "shadingPackagePosition": "TOP", + "shadingPositionAdjustmentActive": null, + "shadingPositionAdjustmentClientId": null, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100BLIND_MODULE00", + "label": "Sonnenschutz Balkont\u00fcr", + "lastStatusUpdate": 1600002124559, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 7, + "modelId": 1, + "modelType": "HmIP-HDM1", + "oem": "HunterDouglas", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100BLIND_MODULE00", + "type": "BLIND_MODULE", + "updateState": "UP_TO_DATE" + }, "3014F7110TILTVIBRATIONSENSOR": { "availableFirmwareVersion": "0.0.0", "connectionType": "HMIP_RF", @@ -7307,6 +7549,11 @@ }, "home": { "accessPointUpdateStates": { + "3014F711A000000BAD0CAAAA": { + "accessPointUpdateState": "UP_TO_DATE", + "successfulUpdateTimestamp": 0, + "updateStateChangedTimestamp": 0 + }, "3014F711A000000BAD0C0DED": { "accessPointUpdateState": "UP_TO_DATE", "successfulUpdateTimestamp": 0, @@ -7449,4 +7696,4 @@ "windSpeed": 8.568 } } -} \ No newline at end of file +} From cdc53329d0d8f322e151e4f2728bb2ac02b48768 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 12 Nov 2020 10:38:53 +0100 Subject: [PATCH 013/430] Add Shelly totalWorkTime as Lamp life (Shelly Air) (#43112) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/const.py | 3 +++ homeassistant/components/shelly/entity.py | 6 ++++++ homeassistant/components/shelly/sensor.py | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index d058a8c4588..36ab095f616 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -19,3 +19,6 @@ SLEEP_PERIOD_MULTIPLIER = 1.2 # Multiplier used to calculate the "update_interval" for non-sleeping devices. UPDATE_PERIOD_MULTIPLIER = 2.2 + +# Shelly Air - Maximum work hours before lamp replacement +SHAIR_MAX_WORK_HOURS = 9000 diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 75070403c2c..d8d4725624d 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -119,6 +119,7 @@ class BlockAttributeDescription: name: str # Callable = lambda attr_info: unit + icon: Optional[str] = None unit: Union[None, str, Callable[[dict], str]] = None value: Callable[[Any], Any] = lambda val: val device_class: Optional[str] = None @@ -254,6 +255,11 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): """Device class of sensor.""" return self.description.device_class + @property + def icon(self): + """Icon of sensor.""" + return self.description.icon + @property def available(self): """Available.""" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index a9a2e8d8d6b..d73d7b5c2d4 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( VOLT, ) +from .const import SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -144,6 +145,15 @@ SENSORS = { device_class=sensor.DEVICE_CLASS_ILLUMINANCE, ), ("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE), + ("relay", "totalWorkTime"): BlockAttributeDescription( + name="Lamp life", + unit=PERCENTAGE, + icon="mdi:progress-wrench", + value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), + device_state_attributes=lambda block: { + "Operational hours": round(block.totalWorkTime / 3600, 1) + }, + ), } REST_SENSORS = { From 24840cce237b618477769d3762b707ee1fb35c16 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Nov 2020 03:00:42 -0700 Subject: [PATCH 014/430] Add a config flow for Recollect Waste (#43063) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/recollect_waste/__init__.py | 84 +++++++++- .../components/recollect_waste/config_flow.py | 64 ++++++++ .../components/recollect_waste/const.py | 11 ++ .../components/recollect_waste/manifest.json | 9 +- .../components/recollect_waste/sensor.py | 145 ++++++++++-------- .../components/recollect_waste/strings.json | 18 +++ .../recollect_waste/translations/en.json | 18 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/recollect_waste/__init__.py | 1 + .../recollect_waste/test_config_flow.py | 91 +++++++++++ 13 files changed, 382 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/recollect_waste/config_flow.py create mode 100644 homeassistant/components/recollect_waste/const.py create mode 100644 homeassistant/components/recollect_waste/strings.json create mode 100644 homeassistant/components/recollect_waste/translations/en.json create mode 100644 tests/components/recollect_waste/__init__.py create mode 100644 tests/components/recollect_waste/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 29991388c08..dd72a42aa77 100644 --- a/.coveragerc +++ b/.coveragerc @@ -711,6 +711,7 @@ omit = homeassistant/components/rainforest_eagle/sensor.py homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* + homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py homeassistant/components/recswitch/switch.py homeassistant/components/reddit/* diff --git a/CODEOWNERS b/CODEOWNERS index f6967a7ed79..bfb147158a6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -361,6 +361,7 @@ homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +homeassistant/components/recollect_waste/* @bachya homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 8ba2fc676f4..57bd346c91b 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1 +1,83 @@ -"""The recollect_waste component.""" +"""The Recollect Waste integration.""" +import asyncio +from datetime import date, timedelta +from typing import List + +from aiorecollect.client import Client, PickupEvent +from aiorecollect.errors import RecollectError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER + +DEFAULT_NAME = "recollect_waste" +DEFAULT_UPDATE_INTERVAL = timedelta(days=1) + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the RainMachine component.""" + hass.data[DOMAIN] = {DATA_COORDINATOR: {}} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up RainMachine as config entry.""" + session = aiohttp_client.async_get_clientsession(hass) + client = Client( + entry.data[CONF_PLACE_ID], entry.data[CONF_SERVICE_ID], session=session + ) + + async def async_get_pickup_events() -> List[PickupEvent]: + """Get the next pickup.""" + try: + return await client.async_get_pickup_events( + start_date=date.today(), end_date=date.today() + timedelta(weeks=4) + ) + except RecollectError as err: + raise UpdateFailed( + f"Error while requesting data from Recollect: {err}" + ) from err + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=f"Place {entry.data[CONF_PLACE_ID]}, Service {entry.data[CONF_SERVICE_ID]}", + update_interval=DEFAULT_UPDATE_INTERVAL, + update_method=async_get_pickup_events, + ) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator + + 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) -> bool: + """Unload an RainMachine 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][DATA_COORDINATOR].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py new file mode 100644 index 00000000000..f0d1527a0fb --- /dev/null +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow for Recollect Waste integration.""" +from aiorecollect.client import Client +from aiorecollect.errors import RecollectError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.helpers import aiohttp_client + +from .const import ( # pylint:disable=unused-import + CONF_PLACE_ID, + CONF_SERVICE_ID, + DOMAIN, + LOGGER, +) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_PLACE_ID): str, vol.Required(CONF_SERVICE_ID): str} +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Recollect Waste.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_import(self, import_config: dict = None) -> dict: + """Handle configuration via YAML import.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input: dict = None) -> dict: + """Handle configuration via the UI.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors={} + ) + + unique_id = f"{user_input[CONF_PLACE_ID]}, {user_input[CONF_SERVICE_ID]}" + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + session = aiohttp_client.async_get_clientsession(self.hass) + client = Client( + user_input[CONF_PLACE_ID], user_input[CONF_SERVICE_ID], session=session + ) + + try: + await client.async_get_next_pickup_event() + except RecollectError as err: + LOGGER.error("Error during setup of integration: %s", err) + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "invalid_place_or_service_id"}, + ) + + return self.async_create_entry( + title=unique_id, + data={ + CONF_PLACE_ID: user_input[CONF_PLACE_ID], + CONF_SERVICE_ID: user_input[CONF_SERVICE_ID], + }, + ) diff --git a/homeassistant/components/recollect_waste/const.py b/homeassistant/components/recollect_waste/const.py new file mode 100644 index 00000000000..8012bdbb02b --- /dev/null +++ b/homeassistant/components/recollect_waste/const.py @@ -0,0 +1,11 @@ +"""Define Recollect Waste constants.""" +import logging + +DOMAIN = "recollect_waste" + +LOGGER = logging.getLogger(__package__) + +CONF_PLACE_ID = "place_id" +CONF_SERVICE_ID = "service_id" + +DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index bed07f919ef..6e1580ddf5c 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -1,7 +1,12 @@ { "domain": "recollect_waste", "name": "ReCollect Waste", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": ["aiorecollect==0.2.1"], - "codeowners": [] + "requirements": [ + "aiorecollect==0.2.1" + ], + "codeowners": [ + "@bachya" + ] } diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index d360ff8f301..7ce75b1e3fa 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,27 +1,30 @@ -"""Support for Recollect Waste curbside collection pickup.""" -from datetime import date, timedelta -import logging +"""Support for Recollect Waste sensors.""" +from typing import Callable -from aiorecollect import Client -from aiorecollect.errors import RecollectError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER -_LOGGER = logging.getLogger(__name__) ATTR_PICKUP_TYPES = "pickup_types" ATTR_AREA_NAME = "area_name" ATTR_NEXT_PICKUP_TYPES = "next_pickup_types" ATTR_NEXT_PICKUP_DATE = "next_pickup_date" -CONF_PLACE_ID = "place_id" -CONF_SERVICE_ID = "service_id" -DEFAULT_NAME = "recollect_waste" -ICON = "mdi:trash-can-outline" -SCAN_INTERVAL = timedelta(days=1) +DEFAULT_ATTRIBUTION = "Pickup data provided by Recollect Waste" +DEFAULT_NAME = "recollect_waste" +DEFAULT_ICON = "mdi:trash-can-outline" + +CONF_NAME = "name" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -32,70 +35,88 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Recollect Waste platform.""" - session = aiohttp_client.async_get_clientsession(hass) - client = Client(config[CONF_PLACE_ID], config[CONF_SERVICE_ID], session=session) - - # Ensure the client can connect to the API successfully - # with given place_id and service_id. - try: - await client.async_get_next_pickup_event() - except RecollectError as err: - _LOGGER.error("Error setting up Recollect sensor platform: %s", err) - return - - async_add_entities([RecollectWasteSensor(config.get(CONF_NAME), client)], True) +async def async_setup_platform( + hass: HomeAssistant, + config: dict, + async_add_entities: Callable, + discovery_info: dict = None, +): + """Import Awair configuration from YAML.""" + LOGGER.warning( + "Loading Recollect Waste via platform setup is deprecated. " + "Please remove it from your configuration." + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -class RecollectWasteSensor(Entity): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Recollect Waste sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + async_add_entities([RecollectWasteSensor(coordinator, entry)]) + + +class RecollectWasteSensor(CoordinatorEntity): """Recollect Waste Sensor.""" - def __init__(self, name, client): + def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" - self._attributes = {} - self._name = name + super().__init__(coordinator) + self._attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._place_id = entry.data[CONF_PLACE_ID] + self._service_id = entry.data[CONF_SERVICE_ID] self._state = None - self.client = client @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self.client.place_id}{self.client.service_id}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_state_attributes(self): + def device_state_attributes(self) -> dict: """Return the state attributes.""" return self._attributes @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend.""" - return ICON + return DEFAULT_ICON - async def async_update(self): - """Update device state.""" - try: - pickup_event_array = await self.client.async_get_pickup_events( - start_date=date.today(), end_date=date.today() + timedelta(weeks=4) - ) - except RecollectError as err: - _LOGGER.error("Error while requesting data from Recollect: %s", err) - return + @property + def name(self) -> str: + """Return the name of the sensor.""" + return DEFAULT_NAME - pickup_event = pickup_event_array[0] - next_pickup_event = pickup_event_array[1] + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self._state + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._place_id}{self._service_id}" + + @callback + def _handle_coordinator_update(self) -> None: + """Respond to a DataUpdateCoordinator update.""" + self.update_from_latest_data() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self.update_from_latest_data() + + @callback + def update_from_latest_data(self) -> None: + """Update the state.""" + pickup_event = self.coordinator.data[0] + next_pickup_event = self.coordinator.data[1] next_date = str(next_pickup_event.date) + self._state = pickup_event.date self._attributes.update( { diff --git a/homeassistant/components/recollect_waste/strings.json b/homeassistant/components/recollect_waste/strings.json new file mode 100644 index 00000000000..0cd251c737b --- /dev/null +++ b/homeassistant/components/recollect_waste/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "place_id": "Place ID", + "service_id": "Service ID" + } + } + }, + "error": { + "invalid_place_or_service_id": "Invalid Place or Service ID" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/recollect_waste/translations/en.json b/homeassistant/components/recollect_waste/translations/en.json new file mode 100644 index 00000000000..28d73d189b8 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_place_or_service_id": "Invalid Place or Service ID" + }, + "step": { + "user": { + "data": { + "place_id": "Place ID", + "service_id": "Service ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 167ece1bcad..d7cd4fd20ba 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -157,6 +157,7 @@ FLOWS = [ "pvpc_hourly_pricing", "rachio", "rainmachine", + "recollect_waste", "rfxtrx", "ring", "risco", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d1887a5ce3..46621831562 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -136,6 +136,9 @@ aiopvpc==2.0.2 # homeassistant.components.webostv aiopylgtv==0.3.3 +# homeassistant.components.recollect_waste +aiorecollect==0.2.1 + # homeassistant.components.shelly aioshelly==0.5.1 diff --git a/tests/components/recollect_waste/__init__.py b/tests/components/recollect_waste/__init__.py new file mode 100644 index 00000000000..0357682f7f9 --- /dev/null +++ b/tests/components/recollect_waste/__init__.py @@ -0,0 +1 @@ +"""Define tests for the Recollet Waste integration.""" diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py new file mode 100644 index 00000000000..bec87b72ee4 --- /dev/null +++ b/tests/components/recollect_waste/test_config_flow.py @@ -0,0 +1,91 @@ +"""Define tests for the Recollect Waste config flow.""" +from aiorecollect.errors import RecollectError + +from homeassistant import data_entry_flow +from homeassistant.components.recollect_waste import ( + CONF_PLACE_ID, + CONF_SERVICE_ID, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + MockConfigEntry(domain=DOMAIN, unique_id="12345, 12345", data=conf).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_invalid_place_or_service_id(hass): + """Test that an invalid Place or Service ID throws an error.""" + conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + with patch( + "aiorecollect.client.Client.async_get_next_pickup_event", + side_effect=RecollectError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_place_or_service_id"} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_import(hass): + """Test that the user step works.""" + conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + with patch( + "homeassistant.components.recollect_waste.async_setup_entry", return_value=True + ), patch( + "aiorecollect.client.Client.async_get_next_pickup_event", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "12345, 12345" + assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + with patch( + "homeassistant.components.recollect_waste.async_setup_entry", return_value=True + ), patch( + "aiorecollect.client.Client.async_get_next_pickup_event", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "12345, 12345" + assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} From 673ac21de400a38aa240afea60064b2ff6c7436a Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 12 Nov 2020 11:50:24 +0100 Subject: [PATCH 015/430] Added missing system health translation for "Views" (#43126) --- homeassistant/components/lovelace/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/strings.json b/homeassistant/components/lovelace/strings.json index 6d79805105d..87f8407d93c 100644 --- a/homeassistant/components/lovelace/strings.json +++ b/homeassistant/components/lovelace/strings.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Dashboards", "mode": "Mode", - "resources": "Resources" + "resources": "Resources", + "views": "Views" } } } From 6f326a7ea4bbe44f0ccaae3709300d7a65c2ce0c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Nov 2020 11:58:28 +0100 Subject: [PATCH 016/430] Add support for multiple event triggers in automation (#43097) Co-authored-by: Paulus Schoutsen --- .../components/automation/__init__.py | 73 +++++-------------- homeassistant/components/automation/config.py | 37 ++++++++-- homeassistant/components/automation/const.py | 19 +++++ .../components/automation/helpers.py | 15 ++++ homeassistant/components/config/automation.py | 7 +- .../homeassistant/triggers/event.py | 17 ++++- .../homeassistant/triggers/test_event.py | 27 +++++++ 7 files changed, 131 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/automation/const.py create mode 100644 homeassistant/components/automation/helpers.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 0989ed43495..5056f225251 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -43,40 +43,35 @@ from homeassistant.helpers.script import ( ATTR_MODE, CONF_MAX, CONF_MAX_EXCEEDED, - SCRIPT_MODE_SINGLE, Script, - make_script_schema, ) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.singleton import singleton from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime +from .config import async_validate_config_item +from .const import ( + CONF_ACTION, + CONF_CONDITION, + CONF_INITIAL_STATE, + CONF_TRIGGER, + DEFAULT_INITIAL_STATE, + DOMAIN, + LOGGER, +) +from .helpers import async_get_blueprints + # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any -DOMAIN = "automation" ENTITY_ID_FORMAT = DOMAIN + ".{}" -DATA_BLUEPRINTS = "automation_blueprints" -CONF_DESCRIPTION = "description" -CONF_HIDE_ENTITY = "hide_entity" - -CONF_CONDITION = "condition" -CONF_ACTION = "action" -CONF_TRIGGER = "trigger" -CONF_CONDITION_TYPE = "condition_type" -CONF_INITIAL_STATE = "initial_state" CONF_SKIP_CONDITION = "skip_condition" CONF_STOP_ACTIONS = "stop_actions" -CONF_BLUEPRINT = "blueprint" -CONF_INPUT = "input" - -DEFAULT_INITIAL_STATE = True DEFAULT_STOP_ACTIONS = True EVENT_AUTOMATION_RELOADED = "automation_reloaded" @@ -87,38 +82,8 @@ ATTR_SOURCE = "source" ATTR_VARIABLES = "variables" SERVICE_TRIGGER = "trigger" -_LOGGER = logging.getLogger(__name__) - AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] -_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) - -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), - make_script_schema( - { - # 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): cv.boolean, - vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, - vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, - vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, - vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, - }, - SCRIPT_MODE_SINGLE, - ), -) - - -@singleton(DATA_BLUEPRINTS) -@callback -def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore - """Get automation blueprints.""" - return blueprint.DomainBlueprints(hass, DOMAIN, _LOGGER) # type: ignore - @bind_hass def is_on(hass, entity_id): @@ -194,7 +159,7 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: async def async_setup(hass, config): """Set up the automation.""" - hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) await _async_process_config(hass, config, component) @@ -263,7 +228,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._is_enabled = False self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None - self._logger = _LOGGER + self._logger = LOGGER self._variables: ScriptVariables = variables @property @@ -536,10 +501,12 @@ async def _async_process_config( try: config_block = cast( Dict[str, Any], - PLATFORM_SCHEMA(blueprint_inputs.async_substitute()), + await async_validate_config_item( + hass, blueprint_inputs.async_substitute() + ), ) except vol.Invalid as err: - _LOGGER.error( + LOGGER.error( "Blueprint %s generated invalid automation with inputs %s: %s", blueprint_inputs.blueprint.name, blueprint_inputs.inputs, @@ -561,7 +528,7 @@ async def _async_process_config( script_mode=config_block[CONF_MODE], max_runs=config_block[CONF_MAX], max_exceeded=config_block[CONF_MAX_EXCEEDED], - logger=_LOGGER, + logger=LOGGER, # We don't pass variables here # Automation will already render them to use them in the condition # and so will pass them on to the script. @@ -600,7 +567,7 @@ async def _async_process_if(hass, config, p_config): try: checks.append(await condition.async_from_config(hass, if_config, False)) except HomeAssistantError as ex: - _LOGGER.warning("Invalid condition: %s", ex) + LOGGER.warning("Invalid condition: %s", ex) return None def if_action(variables=None): diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index c5aa8a62a15..8a13334bc6d 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -8,25 +8,48 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.const import CONF_ALIAS, CONF_ID, CONF_VARIABLES from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, config_validation as cv, script from homeassistant.helpers.condition import async_validate_condition_config -from homeassistant.helpers.script import async_validate_actions_config from homeassistant.helpers.trigger import async_validate_trigger_config from homeassistant.loader import IntegrationNotFound -from . import ( +from .const import ( CONF_ACTION, CONF_CONDITION, + CONF_DESCRIPTION, + CONF_HIDE_ENTITY, + CONF_INITIAL_STATE, CONF_TRIGGER, DOMAIN, - PLATFORM_SCHEMA, - async_get_blueprints, ) +from .helpers import async_get_blueprints # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any +_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), + script.make_script_schema( + { + # 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): cv.boolean, + vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, + vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, + }, + script.SCRIPT_MODE_SINGLE, + ), +) + async def async_validate_config_item(hass, config, full_config=None): """Validate config item.""" @@ -48,7 +71,9 @@ async def async_validate_config_item(hass, config, full_config=None): ] ) - config[CONF_ACTION] = await async_validate_actions_config(hass, config[CONF_ACTION]) + config[CONF_ACTION] = await script.async_validate_actions_config( + hass, config[CONF_ACTION] + ) return config diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py new file mode 100644 index 00000000000..c8db3aa01e5 --- /dev/null +++ b/homeassistant/components/automation/const.py @@ -0,0 +1,19 @@ +"""Constants for the automation integration.""" +import logging + +CONF_CONDITION = "condition" +CONF_ACTION = "action" +CONF_TRIGGER = "trigger" +DOMAIN = "automation" + +CONF_DESCRIPTION = "description" +CONF_HIDE_ENTITY = "hide_entity" + +CONF_CONDITION_TYPE = "condition_type" +CONF_INITIAL_STATE = "initial_state" +CONF_BLUEPRINT = "blueprint" +CONF_INPUT = "input" + +DEFAULT_INITIAL_STATE = True + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py new file mode 100644 index 00000000000..688f051861e --- /dev/null +++ b/homeassistant/components/automation/helpers.py @@ -0,0 +1,15 @@ +"""Helpers for automation integration.""" +from homeassistant.components import blueprint +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +from .const import DOMAIN, LOGGER + +DATA_BLUEPRINTS = "automation_blueprints" + + +@singleton(DATA_BLUEPRINTS) +@callback +def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore + """Get automation blueprints.""" + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) # type: ignore diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 6216a52fc13..01e22297c0d 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -2,8 +2,11 @@ from collections import OrderedDict import uuid -from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA -from homeassistant.components.automation.config import async_validate_config_item +from homeassistant.components.automation.config import ( + DOMAIN, + PLATFORM_SCHEMA, + async_validate_config_item, +) from homeassistant.config import AUTOMATION_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.helpers import config_validation as cv, entity_registry diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index c5baf6ca4b2..b7ab081d266 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -14,7 +14,7 @@ CONF_EVENT_CONTEXT = "context" TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "event", - vol.Required(CONF_EVENT_TYPE): cv.string, + vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EVENT_DATA): dict, vol.Optional(CONF_EVENT_CONTEXT): dict, } @@ -32,7 +32,8 @@ 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_types = config.get(CONF_EVENT_TYPE) + removes = [] event_data_schema = None if config.get(CONF_EVENT_DATA): @@ -82,4 +83,14 @@ async def async_attach_trigger( event.context, ) - return hass.bus.async_listen(event_type, handle_event) + removes = [ + hass.bus.async_listen(event_type, handle_event) for event_type in event_types + ] + + @callback + def remove_listen_events(): + """Remove event listeners.""" + for remove in removes: + remove() + + return remove_listen_events diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index babb2bf4d87..8fedaac3815 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -59,6 +59,33 @@ async def test_if_fires_on_event(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_multiple_events(hass, calls): + """Test the firing of events.""" + context = Context() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": ["test_event", "test2_event"], + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire("test_event", context=context) + await hass.async_block_till_done() + hass.bus.async_fire("test2_event", context=context) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[0].context.parent_id == context.id + assert calls[1].context.parent_id == context.id + + async def test_if_fires_on_event_extra_data(hass, calls, context_with_user): """Test the firing of events still matches with event data and context.""" assert await async_setup_component( From a665e152a95872f648e2ec96e6795811492312b8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 12 Nov 2020 12:11:05 +0100 Subject: [PATCH 017/430] Fix aurora config flow tests (#43128) --- tests/components/aurora/test_config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index 4d611bd3272..2f4b457a9dd 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -35,11 +35,11 @@ async def test_form(hass): result["flow_id"], DATA, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Aurora - Home" assert result2["data"] == DATA - await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -95,6 +95,8 @@ async def test_option_flow(hass): assert not entry.options with patch("homeassistant.components.aurora.async_setup_entry", return_value=True): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init( entry.entry_id, data=None, From bbd7402793a698fa6ddf370e3f39ae9b77fff22a Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 12 Nov 2020 14:34:08 +0100 Subject: [PATCH 018/430] Add reload support to KNX (#42429) * Add reload support to KNX * Changes from review (platform reset + asyncio.gather) * Changes from review (proper asyncio.gather usage) --- homeassistant/components/knx/__init__.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 87ade4955a0..3dd6a97596d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -1,4 +1,5 @@ """Support KNX devices.""" +import asyncio import logging import voluptuous as vol @@ -19,6 +20,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -27,7 +29,11 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import async_integration_yaml_config +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.typing import ServiceCallType from .const import DOMAIN, SupportedPlatforms from .factory import create_knx_device @@ -172,6 +178,28 @@ async def async_setup(hass, config): schema=SERVICE_KNX_SEND_SCHEMA, ) + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Remove all KNX components and load new ones from config.""" + + # First check for config file. If for some reason it is no longer there + # or knx is no longer mentioned, stop the reload. + config = await async_integration_yaml_config(hass, DOMAIN) + + if not config or DOMAIN not in config: + return + + await hass.data[DOMAIN].xknx.stop() + + await asyncio.gather( + *[platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)] + ) + + await async_setup(hass, config) + + async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) + ) + return True From ad06b6b3402941a614d6b08956bd3256b7c7548e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 12 Nov 2020 16:22:51 +0200 Subject: [PATCH 019/430] Revert "shelly_naming" rebase errors (#43134) --- homeassistant/components/shelly/entity.py | 52 +---------------------- homeassistant/components/shelly/utils.py | 51 +++++++++++----------- 2 files changed, 29 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index d8d4725624d..d0e51e8cc12 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -4,7 +4,6 @@ from typing import Any, Callable, Optional, Union import aioshelly -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers import device_registry, entity, update_coordinator @@ -13,53 +12,6 @@ from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST from .utils import get_entity_name, get_rest_value_from_path -def temperature_unit(block_info: dict) -> str: - """Detect temperature unit.""" - if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - -def shelly_naming(self, block, entity_type: str): - """Naming for switch and sensors.""" - - entity_name = self.wrapper.name - if not block: - return f"{entity_name} {self.description.name}" - - channels = 0 - mode = block.type + "s" - if "num_outputs" in self.wrapper.device.shelly: - channels = self.wrapper.device.shelly["num_outputs"] - if ( - self.wrapper.model in ["SHSW-21", "SHSW-25"] - and self.wrapper.device.settings["mode"] == "roller" - ): - channels = 1 - if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly: - channels = self.wrapper.device.shelly["num_emeters"] - if channels > 1 and block.type != "device": - # Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release - if "name" in self.wrapper.device.settings[mode][int(block.channel)]: - entity_name = self.wrapper.device.settings[mode][int(block.channel)]["name"] - else: - entity_name = None - if not entity_name: - if self.wrapper.model == "SHEM-3": - base = ord("A") - else: - base = ord("1") - entity_name = f"{self.wrapper.name} channel {chr(int(block.channel)+base)}" - - if entity_type == "switch": - return entity_name - - if entity_type == "sensor": - return f"{entity_name} {self.description.name}" - - raise ValueError - - async def async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, sensors, sensor_class ): @@ -218,7 +170,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit = unit self._unique_id = f"{super().unique_id}-{self.attribute}" - self._name = shelly_naming(self, block, "sensor") + self._name = get_entity_name(wrapper, block, self.description.name) @property def unique_id(self): @@ -291,7 +243,7 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): self.description = description self._unit = self.description.unit - self._name = shelly_naming(self, None, "sensor") + self._name = get_entity_name(wrapper, None, self.description.name) self.path = self.description.path self._attributes = self.description.attributes diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d6907e55e00..10eacff7068 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -43,34 +43,37 @@ def get_entity_name( """Naming for switch and sensors.""" entity_name = wrapper.name - channels = None - if block.type == "input": - channels = wrapper.device.shelly.get("num_inputs") - elif block.type == "emeter": - channels = wrapper.device.shelly.get("num_emeters") - elif block.type in ["relay", "light"]: - channels = wrapper.device.shelly.get("num_outputs") - elif block.type in ["roller", "device"]: - channels = 1 + if block: + channels = None + if block.type == "input": + channels = wrapper.device.shelly.get("num_inputs") + elif block.type == "emeter": + channels = wrapper.device.shelly.get("num_emeters") + elif block.type in ["relay", "light"]: + channels = wrapper.device.shelly.get("num_outputs") + elif block.type in ["roller", "device"]: + channels = 1 - channels = channels or 1 + channels = channels or 1 - if channels > 1 and block.type != "device": - entity_name = None - mode = block.type + "s" - if mode in wrapper.device.settings: - entity_name = wrapper.device.settings[mode][int(block.channel)].get("name") + if channels > 1 and block.type != "device": + entity_name = None + mode = block.type + "s" + if mode in wrapper.device.settings: + entity_name = wrapper.device.settings[mode][int(block.channel)].get( + "name" + ) - if not entity_name: - if wrapper.model == "SHEM-3": - base = ord("A") - else: - base = ord("1") - entity_name = f"{wrapper.name} channel {chr(int(block.channel)+base)}" + if not entity_name: + if wrapper.model == "SHEM-3": + base = ord("A") + else: + base = ord("1") + entity_name = f"{wrapper.name} channel {chr(int(block.channel)+base)}" - # Shelly Dimmer has two input channels and missing "num_inputs" - if wrapper.model in ["SHDM-1", "SHDM-2"] and block.type == "input": - entity_name = f"{entity_name} channel {int(block.channel)+1}" + # Shelly Dimmer has two input channels and missing "num_inputs" + if wrapper.model in ["SHDM-1", "SHDM-2"] and block.type == "input": + entity_name = f"{entity_name} channel {int(block.channel)+1}" if description: entity_name = f"{entity_name} {description}" From a68d6a63bbe02353128f6d8caf6647693a5af1f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Nov 2020 15:46:49 +0100 Subject: [PATCH 020/430] Add VSCode debug launch conf (#43130) --- .devcontainer/devcontainer.json | 2 +- .pre-commit-config.yaml | 1 + .vscode/launch.json | 15 +++++++++++++++ .vscode/settings.json | 8 ++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fc07d32bfc8..6c17601b98a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,13 +16,13 @@ "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": [ + "!placeholder scalar", "!secret scalar", "!include_dir_named scalar", "!include_dir_list scalar", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43dcb903650..e2fbe9761d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,7 @@ repos: - id: check-executables-have-shebangs stages: [manual] - id: check-json + exclude: .vscode - id: no-commit-to-branch args: - --branch=dev diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..6976e26ebb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "python", + "request": "launch", + "module": "homeassistant", + "args": ["--debug", "-c", "config"] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..910db092e70 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.formatting.provider": "black", + // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings + "python.testing.pytestEnabled": true, + // Added --no-cov to work around TypeError: message must be set + // https://github.com/microsoft/vscode-python/issues/14067 + "python.testing.pytestArgs": ["--no-cov"] +} From 7b7bb0a15f6e1d267c2158ac70df5f051c9641dc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Nov 2020 18:29:06 +0100 Subject: [PATCH 021/430] Bump hatasmota to 0.0.30 (#43140) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index dbe27e1a003..7892b0fc231 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.0.29"], + "requirements": ["hatasmota==0.0.30"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index d30c5b463aa..e13abd71d61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -744,7 +744,7 @@ hass-nabucasa==0.37.1 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.0.29 +hatasmota==0.0.30 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46621831562..2def2f017fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -382,7 +382,7 @@ hangups==0.4.11 hass-nabucasa==0.37.1 # homeassistant.components.tasmota -hatasmota==0.0.29 +hatasmota==0.0.30 # homeassistant.components.jewish_calendar hdate==0.9.12 From 7921be1b5f0f47a89df39b74f6fd17c748119fd7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Nov 2020 10:47:47 -0700 Subject: [PATCH 022/430] Fix incorrect Notion battery state calculation (#43108) * Fix incorrect Notion battery state calculation * Both cases --- homeassistant/components/notion/binary_sensor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index e6f4307a53c..b8fd96fabc5 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -79,14 +79,10 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): """Fetch new state data for the sensor.""" task = self.coordinator.data["tasks"][self._task_id] - if task["task_type"] == SENSOR_BATTERY: - self._state = self.coordinator.data["tasks"][self._task_id]["status"][ - "data" - ]["to_state"] - else: - self._state = self.coordinator.data["tasks"][self._task_id]["status"][ - "value" - ] + if "value" in task["status"]: + self._state = task["status"]["value"] + elif task["task_type"] == SENSOR_BATTERY: + self._state = task["status"]["data"]["to_state"] @property def is_on(self) -> bool: @@ -94,7 +90,7 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): task = self.coordinator.data["tasks"][self._task_id] if task["task_type"] == SENSOR_BATTERY: - return self._state != "critical" + return self._state == "critical" if task["task_type"] in ( SENSOR_DOOR, SENSOR_GARAGE_DOOR, From c620fa344e52ce60ed869121ae4564f61d404ea9 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:10:29 -0300 Subject: [PATCH 023/430] Add support for learning RF commands with Broadlink remotes (#39671) * Add support for learning RF codes with Broadlink remotes * Rename INFRARED and RADIOFREQUENCY to COMMAND_TYPE_IR and COMMAND_TYPE_RF * Rewrite if expression as normal if statement * Use COMMAND_TYPE_IR directly and improve error messages --- homeassistant/components/broadlink/remote.py | 105 +++++++++++++++++-- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index cd86244cea8..5d0d618d7be 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant.components.remote import ( ATTR_ALTERNATIVE, ATTR_COMMAND, + ATTR_COMMAND_TYPE, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, @@ -41,6 +42,10 @@ _LOGGER = logging.getLogger(__name__) LEARNING_TIMEOUT = timedelta(seconds=30) +COMMAND_TYPE_IR = "ir" +COMMAND_TYPE_RF = "rf" +COMMAND_TYPES = [COMMAND_TYPE_IR, COMMAND_TYPE_RF] + CODE_STORAGE_VERSION = 1 FLAG_STORAGE_VERSION = 1 FLAG_SAVE_DELAY = 15 @@ -64,6 +69,7 @@ SERVICE_SEND_SCHEMA = COMMAND_SCHEMA.extend( SERVICE_LEARN_SCHEMA = COMMAND_SCHEMA.extend( { vol.Required(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1)), + vol.Optional(ATTR_COMMAND_TYPE, default=COMMAND_TYPE_IR): vol.In(COMMAND_TYPES), vol.Optional(ATTR_ALTERNATIVE, default=False): cv.boolean, } ) @@ -266,11 +272,11 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): await self._device.async_request(self._device.api.send_data, code) except (AuthorizationError, NetworkTimeoutError, OSError) as err: - _LOGGER.error("Failed to send '%s': %s", command, err) + _LOGGER.error("Failed to send '%s': %s", cmd, err) break except BroadlinkException as err: - _LOGGER.error("Failed to send '%s': %s", command, err) + _LOGGER.error("Failed to send '%s': %s", cmd, err) should_delay = False continue @@ -284,6 +290,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): """Learn a list of commands from a remote.""" kwargs = SERVICE_LEARN_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] + command_type = kwargs[ATTR_COMMAND_TYPE] device = kwargs[ATTR_DEVICE] toggle = kwargs[ATTR_ALTERNATIVE] @@ -293,13 +300,18 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): ) return + if command_type == COMMAND_TYPE_IR: + learn_command = self._async_learn_ir_command + else: + learn_command = self._async_learn_rf_command + should_store = False for command in commands: try: - code = await self._async_learn_command(command) + code = await learn_command(command) if toggle: - code = [code, await self._async_learn_command(command)] + code = [code, await learn_command(command)] except (AuthorizationError, NetworkTimeoutError, OSError) as err: _LOGGER.error("Failed to learn '%s': %s", command, err) @@ -315,8 +327,8 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): if should_store: await self._code_storage.async_save(self._codes) - async def _async_learn_command(self, command): - """Learn a command from a remote.""" + async def _async_learn_ir_command(self, command): + """Learn an infrared command.""" try: await self._device.async_request(self._device.api.enter_learning) @@ -336,12 +348,87 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): await asyncio.sleep(1) try: code = await self._device.async_request(self._device.api.check_data) - except (ReadError, StorageError): continue - return b64encode(code).decode("utf8") - raise TimeoutError("No code received") + + raise TimeoutError( + "No infrared code received within " + f"{LEARNING_TIMEOUT.seconds} seconds" + ) + + finally: + self.hass.components.persistent_notification.async_dismiss( + notification_id="learn_command" + ) + + async def _async_learn_rf_command(self, command): + """Learn a radiofrequency command.""" + try: + await self._device.async_request(self._device.api.sweep_frequency) + + except (BroadlinkException, OSError) as err: + _LOGGER.debug("Failed to sweep frequency: %s", err) + raise + + self.hass.components.persistent_notification.async_create( + f"Press and hold the '{command}' button.", + title="Sweep frequency", + notification_id="sweep_frequency", + ) + + try: + start_time = utcnow() + while (utcnow() - start_time) < LEARNING_TIMEOUT: + await asyncio.sleep(1) + found = await self._device.async_request( + self._device.api.check_frequency + ) + if found: + break + else: + await self._device.async_request( + self._device.api.cancel_sweep_frequency + ) + raise TimeoutError( + "No radiofrequency found within " + f"{LEARNING_TIMEOUT.seconds} seconds" + ) + + finally: + self.hass.components.persistent_notification.async_dismiss( + notification_id="sweep_frequency" + ) + + await asyncio.sleep(1) + + try: + await self._device.async_request(self._device.api.find_rf_packet) + + except (BroadlinkException, OSError) as err: + _LOGGER.debug("Failed to enter learning mode: %s", err) + raise + + self.hass.components.persistent_notification.async_create( + f"Press the '{command}' button again.", + title="Learn command", + notification_id="learn_command", + ) + + try: + start_time = utcnow() + while (utcnow() - start_time) < LEARNING_TIMEOUT: + await asyncio.sleep(1) + try: + code = await self._device.async_request(self._device.api.check_data) + except (ReadError, StorageError): + continue + return b64encode(code).decode("utf8") + + raise TimeoutError( + "No radiofrequency code received within " + f"{LEARNING_TIMEOUT.seconds} seconds" + ) finally: self.hass.components.persistent_notification.async_dismiss( From 31502e960a7f1a96e2286c3301d7a384bf6d376b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 12 Nov 2020 19:49:06 +0100 Subject: [PATCH 024/430] Shelly: minor improvements (#43138) --- homeassistant/components/shelly/__init__.py | 13 ++++++++----- homeassistant/components/shelly/sensor.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 6aa08286dc1..14daffb4221 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -54,6 +54,11 @@ async def get_coap_context(hass): return context +def get_device_name(device): + """Naming for device.""" + return device.settings["name"] or device.settings["device"]["hostname"] + + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Shelly component.""" hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} @@ -126,7 +131,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): super().__init__( hass, _LOGGER, - name=device.settings["name"] or device.settings["device"]["hostname"], + name=get_device_name(device), update_interval=timedelta(seconds=update_interval), ) self.hass = hass @@ -186,7 +191,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): super().__init__( hass, _LOGGER, - name=device.settings["name"] or device.settings["device"]["hostname"], + name=get_device_name(device), update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL), ) self.device = device @@ -195,9 +200,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Fetch data.""" try: async with async_timeout.timeout(5): - _LOGGER.debug( - "REST update for %s", self.device.settings["device"]["hostname"] - ) + _LOGGER.debug("REST update for %s", get_device_name(self.device)) return await self.device.update_status() except OSError as err: raise update_coordinator.UpdateFailed("Error fetching data") from err diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index d73d7b5c2d4..5d29aae8d5e 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -124,6 +124,7 @@ SENSORS = { name="Gas Concentration", unit=CONCENTRATION_PARTS_PER_MILLION, value=lambda value: value, + icon="mdi:gauge", # "sensorOp" is "normal" when the Shelly Gas is working properly and taking measurements. available=lambda block: block.sensorOp == "normal", ), @@ -144,7 +145,7 @@ SENSORS = { unit=LIGHT_LUX, device_class=sensor.DEVICE_CLASS_ILLUMINANCE, ), - ("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE), + ("sensor", "tilt"): BlockAttributeDescription(name="Tilt", unit=DEGREE), ("relay", "totalWorkTime"): BlockAttributeDescription( name="Lamp life", unit=PERCENTAGE, From b84415344f8635270f55d553d5ec52be2a5051a6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 12 Nov 2020 20:15:31 +0100 Subject: [PATCH 025/430] Bump hass-nabucasa to 0.37.2 (#43146) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 81986a3cf6d..f3c79a470ea 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.37.1"], + "requirements": ["hass-nabucasa==0.37.2"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2feef83095d..0fb7dcd3274 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==3.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.37.1 +hass-nabucasa==0.37.2 home-assistant-frontend==20201111.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' diff --git a/requirements_all.txt b/requirements_all.txt index e13abd71d61..60981d79764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.37.1 +hass-nabucasa==0.37.2 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2def2f017fb..f07a3c74795 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -379,7 +379,7 @@ ha-ffmpeg==2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.37.1 +hass-nabucasa==0.37.2 # homeassistant.components.tasmota hatasmota==0.0.30 From 3f09c9bc0ecc91a31f0551a0ccc149a9caaf4c59 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Nov 2020 13:45:20 -0700 Subject: [PATCH 026/430] Fix bug preventing Notion entities from updating their bridge (#43122) --- homeassistant/components/notion/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index c17b68a55b1..296eb34934b 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -249,6 +249,7 @@ class NotionEntity(CoordinatorEntity): @callback def _handle_coordinator_update(self): """Respond to a DataUpdateCoordinator update.""" + self.hass.async_create_task(self._async_update_bridge_id()) self._async_update_from_latest_data() self.async_write_ha_state() From 76b843118b9280b28699c2a24a9c5a2517c870fe Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 13 Nov 2020 00:10:16 +0000 Subject: [PATCH 027/430] [ci skip] Translation update --- .../components/aurora/translations/et.json | 26 +++++++++++++++++++ .../components/aurora/translations/no.json | 26 +++++++++++++++++++ .../components/aurora/translations/pl.json | 26 +++++++++++++++++++ .../components/aurora/translations/ru.json | 26 +++++++++++++++++++ .../components/broadlink/translations/ru.json | 4 +-- .../components/cloud/translations/pl.json | 3 +++ .../components/cloud/translations/ru.json | 13 ++++++++++ .../components/dsmr/translations/ru.json | 10 +++++++ .../components/hassio/translations/et.json | 16 ++++++++++++ .../components/hassio/translations/no.json | 16 ++++++++++++ .../components/hassio/translations/pl.json | 16 ++++++++++++ .../components/hassio/translations/ru.json | 13 ++++++++++ .../homeassistant/translations/et.json | 2 +- .../homeassistant/translations/no.json | 2 +- .../homeassistant/translations/pl.json | 7 ++--- .../homeassistant/translations/ru.json | 20 ++++++++++++++ .../components/locative/translations/ru.json | 2 +- .../components/lovelace/translations/ca.json | 3 ++- .../components/lovelace/translations/cs.json | 3 ++- .../components/lovelace/translations/en.json | 3 ++- .../components/lovelace/translations/et.json | 3 ++- .../components/lovelace/translations/no.json | 3 ++- .../components/lovelace/translations/ru.json | 9 +++++++ .../recollect_waste/translations/ca.json | 18 +++++++++++++ .../recollect_waste/translations/cs.json | 18 +++++++++++++ .../recollect_waste/translations/et.json | 18 +++++++++++++ .../recollect_waste/translations/no.json | 18 +++++++++++++ 27 files changed, 311 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/aurora/translations/et.json create mode 100644 homeassistant/components/aurora/translations/no.json create mode 100644 homeassistant/components/aurora/translations/pl.json create mode 100644 homeassistant/components/aurora/translations/ru.json create mode 100644 homeassistant/components/cloud/translations/ru.json create mode 100644 homeassistant/components/homeassistant/translations/ru.json create mode 100644 homeassistant/components/lovelace/translations/ru.json create mode 100644 homeassistant/components/recollect_waste/translations/ca.json create mode 100644 homeassistant/components/recollect_waste/translations/cs.json create mode 100644 homeassistant/components/recollect_waste/translations/et.json create mode 100644 homeassistant/components/recollect_waste/translations/no.json diff --git a/homeassistant/components/aurora/translations/et.json b/homeassistant/components/aurora/translations/et.json new file mode 100644 index 00000000000..80fb6b21736 --- /dev/null +++ b/homeassistant/components/aurora/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendus nurjus" + }, + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "L\u00e4vi (%)" + } + } + } + }, + "title": "NOAA Aurora andur" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/no.json b/homeassistant/components/aurora/translations/no.json new file mode 100644 index 00000000000..1d22d6cd08b --- /dev/null +++ b/homeassistant/components/aurora/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Terskel (%)" + } + } + } + }, + "title": "NOAA Aurora-sensor" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/pl.json b/homeassistant/components/aurora/translations/pl.json new file mode 100644 index 00000000000..f8786290458 --- /dev/null +++ b/homeassistant/components/aurora/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Pr\u00f3g prawdopodobie\u0144stwa (%)" + } + } + } + }, + "title": "Sensor NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/ru.json b/homeassistant/components/aurora/translations/ru.json new file mode 100644 index 00000000000..20e8f4a184b --- /dev/null +++ b/homeassistant/components/aurora/translations/ru.json @@ -0,0 +1,26 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u043e\u0440\u043e\u0433 (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json index 617c508b8c2..19470d5a66d 100644 --- a/homeassistant/components/broadlink/translations/ru.json +++ b/homeassistant/components/broadlink/translations/ru.json @@ -25,14 +25,14 @@ "title": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "reset": { - "description": "\u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430 \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n 2. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0412\u0430\u0448\u0443 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0441\u0435\u0442\u044c. \n 3. \u041d\u0435 \u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0438 \u0437\u0430\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \n 4. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c.", + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Broadlink. \n 2. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \n 3. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443. \n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432\u043d\u0438\u0437.\n 5. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0437\u0430\u043c\u043e\u043a.", "title": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "unlock": { "data": { "unlock": "\u0414\u0430, \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e." }, - "description": "\u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0432 Home Assistant. \u0425\u043e\u0442\u0438\u0442\u0435 \u0435\u0433\u043e \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c?", + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0432 Home Assistant. \u0425\u043e\u0442\u0438\u0442\u0435 \u0435\u0433\u043e \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c?", "title": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, "user": { diff --git a/homeassistant/components/cloud/translations/pl.json b/homeassistant/components/cloud/translations/pl.json index b7f07c917b8..30aaeeb77d1 100644 --- a/homeassistant/components/cloud/translations/pl.json +++ b/homeassistant/components/cloud/translations/pl.json @@ -7,6 +7,9 @@ "can_reach_cloud_auth": "Dost\u0119p do serwera uwierzytelniania", "google_enabled": "Asystent Google w\u0142\u0105czony", "logged_in": "Zalogowany", + "relayer_connected": "Relayer pod\u0142\u0105czony", + "remote_connected": "Zdalny dost\u0119p pod\u0142\u0105czony", + "remote_enabled": "Zdalny dost\u0119p w\u0142\u0105czony", "subscription_expiration": "Wyga\u015bni\u0119cie subskrypcji" } } diff --git a/homeassistant/components/cloud/translations/ru.json b/homeassistant/components/cloud/translations/ru.json new file mode 100644 index 00000000000..b66e2ca51fa --- /dev/null +++ b/homeassistant/components/cloud/translations/ru.json @@ -0,0 +1,13 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Alexa", + "can_reach_cert_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0432", + "can_reach_cloud": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a Home Assistant Cloud", + "can_reach_cloud_auth": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438", + "google_enabled": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Google", + "logged_in": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443", + "subscription_expiration": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/ru.json b/homeassistant/components/dsmr/translations/ru.json index 4ad85f691be..3bf0cf9f06f 100644 --- a/homeassistant/components/dsmr/translations/ru.json +++ b/homeassistant/components/dsmr/translations/ru.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 DSMR" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index 981cb51c83a..9e5e776013f 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "Seade", + "disk_total": "Kettaruum kokku", + "disk_used": "Kasutatud kettaruum", + "docker_version": "Dockeri versioon", + "healthy": "Korras", + "host_os": "Host-i operatsioonis\u00fcsteem", + "installed_addons": "Paigaldatud lisandmoodulid", + "supervisor_api": "Superviisori API", + "supervisor_version": "Superviisori j\u00e4rk", + "supported": "Toetatud", + "update_channel": "V\u00e4rskenduskanal", + "version_api": "API versioon" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index d8a4c453015..2fb04f5156e 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "Styret", + "disk_total": "Disk totalt", + "disk_used": "Disk brukt", + "docker_version": "Docker-versjon", + "healthy": "Sunn", + "host_os": "Vertsoperativsystem", + "installed_addons": "Installerte tillegg", + "supervisor_api": "API for Supervisor", + "supervisor_version": "Supervisor versjon", + "supported": "St\u00f8ttet", + "update_channel": "Oppdater kanal", + "version_api": "Versjon API" + } + }, "title": "" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 981cb51c83a..10ee7c9d16c 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "Uk\u0142ad", + "disk_total": "Pojemno\u015b\u0107 dysku", + "disk_used": "Pojemno\u015b\u0107 u\u017cyta", + "docker_version": "Wersja Dockera", + "healthy": "Zdrowy", + "host_os": "System operacyjny hosta", + "installed_addons": "Zainstalowane dodatki", + "supervisor_api": "API Supervisora", + "supervisor_version": "Wersja Supervisora", + "supported": "Wspierany", + "update_channel": "Kana\u0142 aktualizacji", + "version_api": "Wersja API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 981cb51c83a..052e1ec21dd 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,3 +1,16 @@ { + "system_health": { + "info": { + "board": "\u041f\u043b\u0430\u0442\u0430", + "docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f Docker", + "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0445\u043e\u0441\u0442\u0430", + "installed_addons": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f", + "supervisor_api": "Supervisor API", + "supervisor_version": "\u0412\u0435\u0440\u0441\u0438\u044f Supervisor", + "supported": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "update_channel": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u0430\u043d\u0430\u043b", + "version_api": "\u0412\u0435\u0440\u0441\u0438\u044f API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 3c355213899..7bdecd0178f 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -8,7 +8,7 @@ "docker_version": "Docker", "host_os": "Home Assistant OS", "installation_type": "Paigalduse t\u00fc\u00fcp", - "os_name": "Operatsioonis\u00fcsteemi nimi", + "os_name": "Operatsioonis\u00fcsteemi j\u00e4rk", "os_version": "Operatsioonis\u00fcsteemi versioon", "python_version": "Pythoni versioon", "supervisor": "Haldur", diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 1962bba483a..72bba59116c 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -8,7 +8,7 @@ "docker_version": "", "host_os": "", "installation_type": "Installasjonstype", - "os_name": "Operativsystemnavn", + "os_name": "Familie for operativsystem", "os_version": "Operativsystemversjon", "python_version": "Python versjon", "supervisor": "", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index 014657a9e3b..e65756cfc83 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -2,12 +2,13 @@ "system_health": { "info": { "arch": "Architektura procesora", - "dev": "Rozw\u00f3j", + "chassis": "Wersja komputera", + "dev": "Wersja rozwojowa", "docker": "Docker", "docker_version": "Wersja Dockera", - "host_os": "Home Assistant OS", + "host_os": "System operacyjny HA", "installation_type": "Typ instalacji", - "os_name": "Nazwa systemu operacyjnego", + "os_name": "Rodzina systemu operacyjnego", "os_version": "Wersja systemu operacyjnego", "python_version": "Wersja Pythona", "supervisor": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json new file mode 100644 index 00000000000..76af5da7d13 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -0,0 +1,20 @@ +{ + "system_health": { + "info": { + "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", + "chassis": "\u0428\u0430\u0441\u0441\u0438", + "dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438", + "docker": "Docker", + "docker_version": "Docker", + "host_os": "Home Assistant OS", + "installation_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", + "os_name": "\u0421\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c", + "os_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b", + "python_version": "\u0412\u0435\u0440\u0441\u0438\u044f Python", + "supervisor": "Supervisor", + "timezone": "\u0427\u0430\u0441\u043e\u0432\u043e\u0439 \u043f\u043e\u044f\u0441", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f", + "virtualenv": "\u0412\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/ru.json b/homeassistant/components/locative/translations/ru.json index a255f7d158f..c9fc9cfd36a 100644 --- a/homeassistant/components/locative/translations/ru.json +++ b/homeassistant/components/locative/translations/ru.json @@ -9,7 +9,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 Locative?", + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", "title": "Locative" } } diff --git a/homeassistant/components/lovelace/translations/ca.json b/homeassistant/components/lovelace/translations/ca.json index 65408175090..7d24e63c329 100644 --- a/homeassistant/components/lovelace/translations/ca.json +++ b/homeassistant/components/lovelace/translations/ca.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Panells", "mode": "Mode", - "resources": "Recursos" + "resources": "Recursos", + "views": "Visualitzacions" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/cs.json b/homeassistant/components/lovelace/translations/cs.json index 32504fa14f4..5c4dc738c6c 100644 --- a/homeassistant/components/lovelace/translations/cs.json +++ b/homeassistant/components/lovelace/translations/cs.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Dashboardy", "mode": "Re\u017eim", - "resources": "Zdroje" + "resources": "Zdroje", + "views": "Zobrazen\u00ed" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/en.json b/homeassistant/components/lovelace/translations/en.json index a1acdafac6a..53c919f51bc 100644 --- a/homeassistant/components/lovelace/translations/en.json +++ b/homeassistant/components/lovelace/translations/en.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Dashboards", "mode": "Mode", - "resources": "Resources" + "resources": "Resources", + "views": "Views" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/et.json b/homeassistant/components/lovelace/translations/et.json index dcc16725ca3..15c253dd4d4 100644 --- a/homeassistant/components/lovelace/translations/et.json +++ b/homeassistant/components/lovelace/translations/et.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Vaated", "mode": "Re\u017eiim", - "resources": "Ressursid" + "resources": "Ressursid", + "views": "Vaated" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/no.json b/homeassistant/components/lovelace/translations/no.json index 09b5322e061..d4b6d939561 100644 --- a/homeassistant/components/lovelace/translations/no.json +++ b/homeassistant/components/lovelace/translations/no.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Dashboards", "mode": "Modus", - "resources": "Ressurser" + "resources": "Ressurser", + "views": "Visninger" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/ru.json b/homeassistant/components/lovelace/translations/ru.json new file mode 100644 index 00000000000..c677c4e7ece --- /dev/null +++ b/homeassistant/components/lovelace/translations/ru.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "dashboards": "\u041f\u0430\u043d\u0435\u043b\u0438", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/ca.json b/homeassistant/components/recollect_waste/translations/ca.json new file mode 100644 index 00000000000..395fe5b9daa --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_place_or_service_id": "ID de lloc o de servei inv\u00e0lid" + }, + "step": { + "user": { + "data": { + "place_id": "ID de lloc", + "service_id": "ID de servei" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/cs.json b/homeassistant/components/recollect_waste/translations/cs.json new file mode 100644 index 00000000000..57bdeaa80da --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_place_or_service_id": "Neplatn\u00e9 m\u00edsto nebo ID slu\u017eby" + }, + "step": { + "user": { + "data": { + "place_id": "ID m\u00edsta", + "service_id": "ID slu\u017eby" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/et.json b/homeassistant/components/recollect_waste/translations/et.json new file mode 100644 index 00000000000..e1402d12e42 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "invalid_place_or_service_id": "Sobimatu asukoha v\u00f5i teenuse ID" + }, + "step": { + "user": { + "data": { + "place_id": "Asukoha ID", + "service_id": "Teenuse ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/no.json b/homeassistant/components/recollect_waste/translations/no.json new file mode 100644 index 00000000000..6c4932505ba --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "invalid_place_or_service_id": "Ugyldig sted eller tjeneste ID" + }, + "step": { + "user": { + "data": { + "place_id": "Sted ID", + "service_id": "Tjeneste ID" + } + } + } + } +} \ No newline at end of file From e0f1d0ab20fddbe3f5d14148ba504f6446417340 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 13 Nov 2020 09:31:55 +0100 Subject: [PATCH 028/430] Mock time_date sensor tests (#43141) --- homeassistant/components/time_date/sensor.py | 14 +++++++++----- tests/components/time_date/test_sensor.py | 15 ++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index b9012385296..8be0e2d8c66 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -80,7 +80,7 @@ class TimeDateSensor(Entity): return "mdi:clock" async def async_added_to_hass(self) -> None: - """Set up next update.""" + """Set up first update.""" self.unsub = async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.get_next_interval() ) @@ -91,20 +91,24 @@ class TimeDateSensor(Entity): self.unsub() self.unsub = None - def get_next_interval(self, now=None): + def get_next_interval(self): """Compute next time an update should occur.""" - if now is None: - now = dt_util.utcnow() + now = dt_util.utcnow() + if self.type == "date": now = dt_util.start_of_local_day(dt_util.as_local(now)) return now + timedelta(seconds=86400) + if self.type == "beat": interval = 86.4 else: interval = 60 timestamp = int(dt_util.as_timestamp(now)) delta = interval - (timestamp % interval) - return now + timedelta(seconds=delta) + next_interval = now + timedelta(seconds=delta) + _LOGGER.debug("%s + %s -> %s (%s)", now, delta, next_interval, self.type) + + return next_interval def _update_internal_state(self, time_date): time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT) diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index a20afb61fe3..b5116478678 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -21,17 +21,20 @@ async def test_intervals(hass): """Test timing intervals of sensors.""" device = time_date.TimeDateSensor(hass, "time") now = dt_util.utc_from_timestamp(45) - next_time = device.get_next_interval(now) + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() assert next_time == dt_util.utc_from_timestamp(60) device = time_date.TimeDateSensor(hass, "beat") now = dt_util.utc_from_timestamp(29) - next_time = device.get_next_interval(now) + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() assert next_time == dt_util.utc_from_timestamp(86.4) device = time_date.TimeDateSensor(hass, "date_time") now = dt_util.utc_from_timestamp(1495068899) - next_time = device.get_next_interval(now) + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() assert next_time == dt_util.utc_from_timestamp(1495068900) now = dt_util.utcnow() @@ -117,7 +120,8 @@ async def test_timezone_intervals(hass): device = time_date.TimeDateSensor(hass, "date") now = dt_util.utc_from_timestamp(50000) - next_time = device.get_next_interval(now) + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() # start of local day in EST was 18000.0 # so the second day was 18000 + 86400 assert next_time.timestamp() == 104400 @@ -127,7 +131,8 @@ async def test_timezone_intervals(hass): dt_util.set_default_time_zone(new_tz) now = dt_util.parse_datetime("2017-11-13 19:47:19-07:00") device = time_date.TimeDateSensor(hass, "date") - next_time = device.get_next_interval(now) + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2017-11-14 00:00:00-07:00") From 1a888433cb4146370b77096f4001fc46ca33c1ae Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 13 Nov 2020 09:33:00 +0100 Subject: [PATCH 029/430] Fix beat calculation (#43142) --- homeassistant/components/time_date/sensor.py | 23 +++++++++++--------- tests/components/time_date/test_sensor.py | 2 ++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 8be0e2d8c66..86ff3531240 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -116,16 +116,6 @@ class TimeDateSensor(Entity): 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) - delta = timedelta( - hours=time_bmt.hour, - minutes=time_bmt.minute, - seconds=time_bmt.second, - microseconds=time_bmt.microsecond, - ) - beat = int((delta.seconds + delta.microseconds / 1000000.0) / 86.4) - if self.type == "time": self._state = time elif self.type == "date": @@ -139,6 +129,19 @@ class TimeDateSensor(Entity): elif self.type == "time_utc": self._state = time_utc elif self.type == "beat": + # Calculate Swatch Internet Time. + time_bmt = time_date + timedelta(hours=1) + delta = timedelta( + hours=time_bmt.hour, + minutes=time_bmt.minute, + seconds=time_bmt.second, + microseconds=time_bmt.microsecond, + ) + + # Use integers to better handle rounding. For example, + # int(63763.2/86.4) = 737 but 637632//864 = 738. + beat = int(delta.total_seconds() * 10) // 864 + self._state = f"@{beat:03d}" elif self.type == "date_time_iso": self._state = dt_util.parse_datetime(f"{date} {time}").isoformat() diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index b5116478678..bf55ea8075b 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -69,6 +69,8 @@ async def test_states(hass): device = time_date.TimeDateSensor(hass, "beat") device._update_internal_state(now) assert device.state == "@079" + device._update_internal_state(dt_util.utc_from_timestamp(1602952963.2)) + assert device.state == "@738" device = time_date.TimeDateSensor(hass, "date_time_iso") device._update_internal_state(now) From 899fc3d35a2db6ba4feffef02395f7f602cd79fe Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 13 Nov 2020 09:37:45 +0100 Subject: [PATCH 030/430] Update xknx to 0.15.3 (#42026) Co-authored-by: Paulus Schoutsen --- homeassistant/components/knx/binary_sensor.py | 10 +++++ homeassistant/components/knx/climate.py | 38 +++++++++---------- homeassistant/components/knx/const.py | 8 +++- homeassistant/components/knx/factory.py | 6 ++- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 24 +++++++++--- homeassistant/components/knx/sensor.py | 10 +++++ requirements_all.txt | 2 +- 8 files changed, 70 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index a62e95f1def..35feb09dc1d 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -41,3 +41,13 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes.""" return {ATTR_COUNTER: self._device.counter} + + @property + def force_update(self) -> bool: + """ + Return True if state updates should be forced. + + If True, a state change will be triggered anytime the state property is + updated, not just when the value changes. + """ + return self._device.ignore_internal_state diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 1960627a8d6..070f635cc48 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -2,7 +2,7 @@ from typing import List, Optional from xknx.devices import Climate as XknxClimate -from xknx.dpt import HVACOperationMode +from xknx.dpt import HVACControllerMode, HVACOperationMode from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -14,11 +14,11 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from .const import DOMAIN, OPERATION_MODES, PRESET_MODES +from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES from .knx_entity import KnxEntity -OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items()) -PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items()) +CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} +PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -92,27 +92,27 @@ class KNXClimate(KnxEntity, ClimateEntity): """Return current operation ie. heat, cool, idle.""" if self._device.supports_on_off and not self._device.is_on: return HVAC_MODE_OFF - if self._device.mode.supports_operation_mode: - return OPERATION_MODES.get( - self._device.mode.operation_mode.value, HVAC_MODE_HEAT + if self._device.mode.supports_controller_mode: + return CONTROLLER_MODES.get( + self._device.mode.controller_mode.value, HVAC_MODE_HEAT ) # default to "heat" return HVAC_MODE_HEAT @property def hvac_modes(self) -> Optional[List[str]]: - """Return the list of available operation modes.""" - _operations = [ - OPERATION_MODES.get(operation_mode.value) - for operation_mode in self._device.mode.operation_modes + """Return the list of available operation/controller modes.""" + _controller_modes = [ + CONTROLLER_MODES.get(controller_mode.value) + for controller_mode in self._device.mode.controller_modes ] if self._device.supports_on_off: - if not _operations: - _operations.append(HVAC_MODE_HEAT) - _operations.append(HVAC_MODE_OFF) + if not _controller_modes: + _controller_modes.append(HVAC_MODE_HEAT) + _controller_modes.append(HVAC_MODE_OFF) - _modes = list(set(filter(None, _operations))) + _modes = list(set(filter(None, _controller_modes))) # default to ["heat"] return _modes if _modes else [HVAC_MODE_HEAT] @@ -123,11 +123,11 @@ class KNXClimate(KnxEntity, ClimateEntity): else: if self._device.supports_on_off and not self._device.is_on: await self._device.turn_on() - if self._device.mode.supports_operation_mode: - knx_operation_mode = HVACOperationMode( - OPERATION_MODES_INV.get(hvac_mode) + if self._device.mode.supports_controller_mode: + knx_controller_mode = HVACControllerMode( + CONTROLLER_MODES_INV.get(hvac_mode) ) - await self._device.mode.set_operation_mode(knx_operation_mode) + await self._device.mode.set_controller_mode(knx_controller_mode) self.async_write_ha_state() @property diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 8b0dd90393b..e434aed395d 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -11,13 +11,16 @@ from homeassistant.components.climate.const import ( PRESET_AWAY, PRESET_COMFORT, PRESET_ECO, + PRESET_NONE, PRESET_SLEEP, ) DOMAIN = "knx" +CONF_INVERT = "invert" CONF_STATE_ADDRESS = "state_address" CONF_SYNC_STATE = "sync_state" +CONF_RESET_AFTER = "reset_after" class ColorTempModes(Enum): @@ -41,8 +44,8 @@ class SupportedPlatforms(Enum): weather = "weather" -# Map KNX operation modes to HA modes. This list might not be complete. -OPERATION_MODES = { +# Map KNX controller modes to HA modes. This list might not be complete. +CONTROLLER_MODES = { # Map DPT 20.105 HVAC control modes "Auto": HVAC_MODE_AUTO, "Heat": HVAC_MODE_HEAT, @@ -54,6 +57,7 @@ OPERATION_MODES = { PRESET_MODES = { # Map DPT 20.102 HVAC operating modes to HA presets + "Auto": PRESET_NONE, "Frost Protection": PRESET_ECO, "Night": PRESET_SLEEP, "Standby": PRESET_AWAY, diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 3334e49ce38..6da2b73cd6a 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -164,6 +164,7 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS ), operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES), + controller_modes=config.get(ClimateSchema.CONF_CONTROLLER_MODES), ) return XknxClimate( @@ -202,6 +203,7 @@ def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch: name=config[CONF_NAME], group_address=config[CONF_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), + invert=config.get(SwitchSchema.CONF_INVERT), ) @@ -212,6 +214,7 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: name=config[CONF_NAME], group_address_state=config[SensorSchema.CONF_STATE_ADDRESS], sync_state=config[SensorSchema.CONF_SYNC_STATE], + always_callback=config[SensorSchema.CONF_ALWAYS_CALLBACK], value_type=config[CONF_TYPE], ) @@ -243,10 +246,11 @@ def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySen knx_module, name=device_name, group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], + invert=config.get(BinarySensorSchema.CONF_INVERT), sync_state=config[BinarySensorSchema.CONF_SYNC_STATE], device_class=config.get(CONF_DEVICE_CLASS), ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE], - context_timeout=config[BinarySensorSchema.CONF_CONTEXT_TIMEOUT], + context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), ) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 2d387f0653d..4055048fd2d 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.15.0"], + "requirements": ["xknx==0.15.3"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 84a54536db5..cbf06925163 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -15,9 +15,11 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from .const import ( + CONF_INVERT, + CONF_RESET_AFTER, CONF_STATE_ADDRESS, CONF_SYNC_STATE, - OPERATION_MODES, + CONTROLLER_MODES, PRESET_MODES, ColorTempModes, ) @@ -84,9 +86,10 @@ class BinarySensorSchema: CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_SYNC_STATE = CONF_SYNC_STATE + CONF_INVERT = CONF_INVERT CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state" CONF_CONTEXT_TIMEOUT = "context_timeout" - CONF_RESET_AFTER = "reset_after" + CONF_RESET_AFTER = CONF_RESET_AFTER DEFAULT_NAME = "KNX Binary Sensor" @@ -101,12 +104,13 @@ class BinarySensorSchema: cv.boolean, cv.string, ), - vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=True): cv.boolean, - vol.Optional(CONF_CONTEXT_TIMEOUT, default=1.0): vol.All( + vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean, + vol.Required(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Coerce(float), vol.Range(min=0, max=10) ), - vol.Required(CONF_STATE_ADDRESS): cv.string, vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_INVERT): cv.boolean, vol.Optional(CONF_RESET_AFTER): cv.positive_int, } ), @@ -187,6 +191,7 @@ class ClimateSchema: CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address" CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address" CONF_OPERATION_MODES = "operation_modes" + CONF_CONTROLLER_MODES = "controller_modes" CONF_ON_OFF_ADDRESS = "on_off_address" CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" CONF_ON_OFF_INVERT = "on_off_invert" @@ -240,7 +245,10 @@ class ClimateSchema: 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, **PRESET_MODES})] + cv.ensure_list, [vol.In({**PRESET_MODES})] + ), + vol.Optional(CONF_CONTROLLER_MODES): vol.All( + cv.ensure_list, [vol.In({**CONTROLLER_MODES})] ), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), @@ -252,6 +260,7 @@ class ClimateSchema: class SwitchSchema: """Voluptuous schema for KNX switches.""" + CONF_INVERT = CONF_INVERT CONF_STATE_ADDRESS = CONF_STATE_ADDRESS DEFAULT_NAME = "KNX Switch" @@ -260,6 +269,7 @@ class SwitchSchema: vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_INVERT): cv.boolean, } ) @@ -299,6 +309,7 @@ class NotifySchema: class SensorSchema: """Voluptuous schema for KNX sensors.""" + CONF_ALWAYS_CALLBACK = "always_callback" CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_SYNC_STATE = CONF_SYNC_STATE DEFAULT_NAME = "KNX Sensor" @@ -311,6 +322,7 @@ class SensorSchema: cv.boolean, cv.string, ), + vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean, vol.Required(CONF_STATE_ADDRESS): cv.string, vol.Required(CONF_TYPE): vol.Any(int, float, str), } diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index fc2cbced8bb..dc9ffcb61b7 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -41,3 +41,13 @@ class KNXSensor(KnxEntity, Entity): if device_class in DEVICE_CLASSES: return device_class return None + + @property + def force_update(self) -> bool: + """ + Return True if state updates should be forced. + + If True, a state change will be triggered anytime the state property is + updated, not just when the value changes. + """ + return self._device.always_callback diff --git a/requirements_all.txt b/requirements_all.txt index 60981d79764..990c6ed969c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2304,7 +2304,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.15.0 +xknx==0.15.3 # homeassistant.components.bluesound # homeassistant.components.rest From ad7f1446be59117e25b5d214b85ee568021bd784 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 13 Nov 2020 01:38:58 -0700 Subject: [PATCH 031/430] Streamline SimpliSafe errors (#43117) --- .../components/simplisafe/__init__.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index e06e0b113ab..2430aad43cf 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -534,8 +534,7 @@ class SimpliSafe: ) ) - LOGGER.error("Update failed with stored refresh token") - raise UpdateFailed from result + raise UpdateFailed("Update failed with stored refresh token") LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") self._emergency_refresh_token_used = True @@ -546,23 +545,18 @@ class SimpliSafe: ) return except SimplipyError as err: - LOGGER.error("Error while using stored refresh token: %s", err) - raise UpdateFailed from err + raise UpdateFailed( # pylint: disable=raise-missing-from + f"Error while using stored refresh token: {err}" + ) if isinstance(result, EndpointUnavailable): - # In case the user attempt an action not allowed in their current plan, + # In case the user attempts an action not allowed in their current plan, # we merely log that message at INFO level (so the user is aware, # but not spammed with ERROR messages that they cannot change): LOGGER.info(result) - raise UpdateFailed from result if isinstance(result, SimplipyError): - LOGGER.error("SimpliSafe error while updating: %s", result) - raise UpdateFailed from result - - if isinstance(result, Exception): - LOGGER.error("Unknown error while updating: %s", result) - raise UpdateFailed from result + raise UpdateFailed(f"SimpliSafe error while updating: {result}") if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: _async_save_refresh_token( From f50869d62ed0e616006e76250045b033accbb376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 13 Nov 2020 09:39:36 +0100 Subject: [PATCH 032/430] Add missing 'hassio' translation string (#43127) * Add missing 'hassio' translation string * Fix typo... --- .../components/homeassistant/strings.json | 1 + .../homeassistant/translations/en.json | 37 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 1aa414c4984..7da4a5a9d8a 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -4,6 +4,7 @@ "arch": "CPU Architecture", "dev": "Development", "docker": "Docker", + "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", "os_version": "Operating System Version", diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 3eb1f91eefc..45ac7092d57 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,20 +1,21 @@ { - "system_health": { - "info": { - "arch": "CPU Architecture", - "chassis": "Chassis", - "dev": "Development", - "docker": "Docker", - "docker_version": "Docker", - "host_os": "Home Assistant OS", - "installation_type": "Installation Type", - "os_name": "Operating System Family", - "os_version": "Operating System Version", - "python_version": "Python Version", - "supervisor": "Supervisor", - "timezone": "Timezone", - "version": "Version", - "virtualenv": "Virtual Environment" - } + "system_health": { + "info": { + "arch": "CPU Architecture", + "chassis": "Chassis", + "dev": "Development", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "Installation Type", + "os_name": "Operating System Family", + "os_version": "Operating System Version", + "python_version": "Python Version", + "supervisor": "Supervisor", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" } -} \ No newline at end of file + } +} From 96641cab02895cba1af6ac22067b265c52314184 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 13 Nov 2020 09:40:00 +0100 Subject: [PATCH 033/430] Fix playing of Spotify URIs on Sonos (#43154) --- 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 659d9bd1b1d..49826ebc410 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.35"], + "requirements": ["pysonos==0.0.36"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index 990c6ed969c..d7991cf8e55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1698,7 +1698,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.35 +pysonos==0.0.36 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f07a3c74795..31d24ded554 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ pysmartthings==0.7.4 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.35 +pysonos==0.0.36 # homeassistant.components.spc pyspcwebgw==0.4.0 From 4907996dd6dfba06fc2b88fe12554183533c1d7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Nov 2020 22:40:25 -1000 Subject: [PATCH 034/430] Guard against empty ssdp locations (#43156) --- homeassistant/components/ssdp/__init__.py | 5 ++++- tests/components/ssdp/test_init.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b3d8a7f2898..e962c141bef 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -92,7 +92,10 @@ class Scanner: entries_to_process.append(entry) - if entry.location not in self._description_cache: + if ( + entry.location is not None + and entry.location not in self._description_cache + ): unseen_locations.add(entry.location) if not entries_to_process: diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index b6c8266b5da..008995cd78d 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -10,7 +10,7 @@ from homeassistant.components import ssdp from tests.common import mock_coro -async def test_scan_match_st(hass): +async def test_scan_match_st(hass, caplog): """Test matching based on ST.""" scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]}) @@ -38,6 +38,7 @@ async def test_scan_match_st(hass): ssdp.ATTR_SSDP_SERVER: "mock-server", ssdp.ATTR_SSDP_EXT: "", } + assert "Failed to fetch ssdp data" not in caplog.text @pytest.mark.parametrize( From 3404e6ded1c164712579f0308a96987f704fed16 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 10:40:51 +0100 Subject: [PATCH 035/430] Update translations --- .../components/aurora/translations/es.json | 26 +++++++++++++ .../components/dsmr/translations/es.json | 10 +++++ .../components/hassio/translations/es.json | 15 ++++++++ .../homeassistant/translations/en.json | 38 +++++++++---------- .../homeassistant/translations/es.json | 1 + .../components/lovelace/translations/es.json | 3 +- .../components/lovelace/translations/pl.json | 3 +- .../components/lovelace/translations/ru.json | 3 +- .../recollect_waste/translations/es.json | 18 +++++++++ .../recollect_waste/translations/pl.json | 18 +++++++++ .../recollect_waste/translations/ru.json | 18 +++++++++ .../components/shelly/translations/pl.json | 4 +- 12 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/aurora/translations/es.json create mode 100644 homeassistant/components/recollect_waste/translations/es.json create mode 100644 homeassistant/components/recollect_waste/translations/pl.json create mode 100644 homeassistant/components/recollect_waste/translations/ru.json diff --git a/homeassistant/components/aurora/translations/es.json b/homeassistant/components/aurora/translations/es.json new file mode 100644 index 00000000000..c722c95ef6f --- /dev/null +++ b/homeassistant/components/aurora/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Umbral (%)" + } + } + } + }, + "title": "Sensor Aurora NOAA" +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index e8e23bf8343..364953d39d6 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Tiempo m\u00ednimo entre actualizaciones de entidad [s]" + }, + "title": "Opciones DSMR" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 981cb51c83a..4c8223f606b 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "board": "Placa", + "disk_total": "Disco total", + "disk_used": "Disco usado", + "docker_version": "Versi\u00f3n de Docker", + "host_os": "Sistema operativo host", + "installed_addons": "Complementos instalados", + "supervisor_api": "API del Supervisor", + "supervisor_version": "Versi\u00f3n del Supervisor", + "supported": "Soportado", + "update_channel": "Actualizar canal", + "version_api": "Versi\u00f3n del API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 45ac7092d57..22538ad6536 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,21 +1,21 @@ { - "system_health": { - "info": { - "arch": "CPU Architecture", - "chassis": "Chassis", - "dev": "Development", - "docker": "Docker", - "docker_version": "Docker", - "hassio": "Supervisor", - "host_os": "Home Assistant OS", - "installation_type": "Installation Type", - "os_name": "Operating System Family", - "os_version": "Operating System Version", - "python_version": "Python Version", - "supervisor": "Supervisor", - "timezone": "Timezone", - "version": "Version", - "virtualenv": "Virtual Environment" + "system_health": { + "info": { + "arch": "CPU Architecture", + "chassis": "Chassis", + "dev": "Development", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "Installation Type", + "os_name": "Operating System Family", + "os_version": "Operating System Version", + "python_version": "Python Version", + "supervisor": "Supervisor", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 13d1c5edcde..1829d16d510 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -6,6 +6,7 @@ "dev": "Desarrollo", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", "host_os": "SO Home Assistant", "installation_type": "Tipo de instalaci\u00f3n", "os_name": "Nombre del Sistema Operativo", diff --git a/homeassistant/components/lovelace/translations/es.json b/homeassistant/components/lovelace/translations/es.json index 108d4f881f0..575610c96e0 100644 --- a/homeassistant/components/lovelace/translations/es.json +++ b/homeassistant/components/lovelace/translations/es.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Paneles de control", "mode": "Modo", - "resources": "Recursos" + "resources": "Recursos", + "views": "Vistas" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/pl.json b/homeassistant/components/lovelace/translations/pl.json index 62f000982ea..3c87f145220 100644 --- a/homeassistant/components/lovelace/translations/pl.json +++ b/homeassistant/components/lovelace/translations/pl.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Dashboardy", "mode": "Tryb", - "resources": "Zasoby" + "resources": "Zasoby", + "views": "Widoki" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/ru.json b/homeassistant/components/lovelace/translations/ru.json index c677c4e7ece..856237d9b1f 100644 --- a/homeassistant/components/lovelace/translations/ru.json +++ b/homeassistant/components/lovelace/translations/ru.json @@ -3,7 +3,8 @@ "info": { "dashboards": "\u041f\u0430\u043d\u0435\u043b\u0438", "mode": "\u0420\u0435\u0436\u0438\u043c", - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b" + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b", + "views": "\u0412\u043a\u043b\u0430\u0434\u043a\u0438" } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json new file mode 100644 index 00000000000..5771c9da9a9 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_place_or_service_id": "ID de servicio o lugar no v\u00e1lidos" + }, + "step": { + "user": { + "data": { + "place_id": "ID de lugar", + "service_id": "ID de servicio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/pl.json b/homeassistant/components/recollect_waste/translations/pl.json new file mode 100644 index 00000000000..013d0028790 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_place_or_service_id": "Nieprawid\u0142owy identyfikator \"Place\" lub \"Service\"" + }, + "step": { + "user": { + "data": { + "place_id": "Identyfikator \"Place\" (place_id)", + "service_id": "Identyfikator \"Service\" (service_id)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/ru.json b/homeassistant/components/recollect_waste/translations/ru.json new file mode 100644 index 00000000000..21e926ec9e1 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_place_or_service_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043b\u0443\u0436\u0431\u044b." + }, + "step": { + "user": { + "data": { + "place_id": "ID \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "service_id": "ID \u0441\u043b\u0443\u0436\u0431\u044b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 4021ac79772..ebf6041d4ba 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nPrzed konfiguracj\u0105, urz\u0105dzenia zasilane bateryjnie nale\u017cy wybudzi\u0107, naciskaj\u0105c przycisk na urz\u0105dzeniu." + "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nPrzed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Przed konfiguracj\u0105, urz\u0105dzenia zasilane bateryjnie nale\u017cy wybudzi\u0107, naciskaj\u0105c przycisk na urz\u0105dzeniu." + "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu." } } } From 890d7400931fffc115641dae283b72e27d7d20fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 10:51:27 +0100 Subject: [PATCH 036/430] Further improve MFI tests (#43167) --- tests/components/mfi/test_sensor.py | 3 ++- tests/components/mfi/test_switch.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 966dcbfd567..ad23039237c 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -97,7 +97,8 @@ async def test_setup_adds_proper_devices(hass): "homeassistant.components.mfi.sensor.MfiSensor", side_effect=mfi.MfiSensor ) as mock_sensor: ports = { - i: mock.MagicMock(model=model) for i, model in enumerate(mfi.SENSOR_MODELS) + i: mock.MagicMock(model=model, label=f"Port {i}", value=0) + for i, model in enumerate(mfi.SENSOR_MODELS) } ports["bad"] = mock.MagicMock(model="notasensor") mock_client.return_value.get_devices.return_value = [ diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index e700aad8353..b11dcdccb6e 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -31,7 +31,10 @@ async def test_setup_adds_proper_devices(hass): "homeassistant.components.mfi.switch.MfiSwitch", side_effect=mfi.MfiSwitch ) as mock_switch: ports = { - i: mock.MagicMock(model=model) for i, model in enumerate(mfi.SWITCH_MODELS) + i: mock.MagicMock( + model=model, label=f"Port {i}", output=False, data={}, ident=f"abcd-{i}" + ) + for i, model in enumerate(mfi.SWITCH_MODELS) } ports["bad"] = mock.MagicMock(model="notaswitch") print(ports["bad"].model) From 7bcd92172af13919f566783416b5e27719a544ea Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 13 Nov 2020 13:15:37 +0100 Subject: [PATCH 037/430] Remove relative time sensor from cert_expiry (#42338) --- .../components/cert_expiry/sensor.py | 31 ---------- tests/components/cert_expiry/test_init.py | 9 +-- tests/components/cert_expiry/test_sensors.py | 56 ++++--------------- 3 files changed, 15 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 3d993fb2bc0..0e329b1898f 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -10,13 +10,11 @@ from homeassistant.const import ( CONF_PORT, DEVICE_CLASS_TIMESTAMP, EVENT_HOMEASSISTANT_START, - TIME_DAYS, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import dt from .const import DEFAULT_PORT, DOMAIN @@ -55,7 +53,6 @@ async def async_setup_entry(hass, entry, async_add_entities): coordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ - SSLCertificateDays(coordinator), SSLCertificateTimestamp(coordinator), ] @@ -79,34 +76,6 @@ class CertExpiryEntity(CoordinatorEntity): } -class SSLCertificateDays(CertExpiryEntity): - """Implementation of the Cert Expiry days sensor.""" - - @property - def name(self): - """Return the name of the sensor.""" - return f"Cert Expiry ({self.coordinator.name})" - - @property - def state(self): - """Return the state of the sensor.""" - if not self.coordinator.is_cert_valid: - return 0 - - expiry = self.coordinator.data - dt.utcnow() - return expiry.days - - @property - def unique_id(self): - """Return a unique id for the sensor.""" - return f"{self.coordinator.host}:{self.coordinator.port}" - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return TIME_DAYS - - class SSLCertificateTimestamp(CertExpiryEntity): """Implementation of the Cert Expiry timestamp sensor.""" diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 2f5e4ce9a1c..6aa8568a9d1 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -76,21 +76,22 @@ async def test_unload_config_entry(mock_now, hass): assert len(config_entries) == 1 assert entry is config_entries[0] + timestamp = future_timestamp(100) with patch( "homeassistant.components.cert_expiry.get_cert_expiry_timestamp", - return_value=future_timestamp(100), + return_value=timestamp, ): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() assert entry.state == ENTRY_STATE_LOADED - state = hass.states.get("sensor.cert_expiry_example_com") - assert state.state == "100" + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + assert state.state == timestamp.isoformat() assert state.attributes.get("error") == "None" assert state.attributes.get("is_valid") await hass.config_entries.async_unload(entry.entry_id) assert entry.state == ENTRY_STATE_NOT_LOADED - state = hass.states.get("sensor.cert_expiry_example_com") + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is None diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 76c6716411b..4a78f02b39c 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -34,13 +34,6 @@ async def test_async_setup_entry(mock_now, hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "100" - assert state.attributes.get("error") == "None" - assert state.attributes.get("is_valid") - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is not None assert state.state != STATE_UNAVAILABLE @@ -65,10 +58,9 @@ async def test_async_setup_entry_bad_cert(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is not None assert state.state != STATE_UNAVAILABLE - assert state.state == "0" assert state.attributes.get("error") == "some error" assert not state.attributes.get("is_valid") @@ -99,7 +91,7 @@ async def test_async_setup_entry_host_unavailable(hass): ): await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is None @@ -122,13 +114,6 @@ async def test_update_sensor(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "100" - assert state.attributes.get("error") == "None" - assert state.attributes.get("is_valid") - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is not None assert state.state != STATE_UNAVAILABLE @@ -144,13 +129,6 @@ async def test_update_sensor(hass): async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "99" - assert state.attributes.get("error") == "None" - assert state.attributes.get("is_valid") - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is not None assert state.state != STATE_UNAVAILABLE @@ -178,13 +156,6 @@ async def test_update_sensor_network_errors(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "100" - assert state.attributes.get("error") == "None" - assert state.attributes.get("is_valid") - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is not None assert state.state != STATE_UNAVAILABLE @@ -203,7 +174,7 @@ async def test_update_sensor_network_errors(hass): next_update = starting_time + timedelta(hours=48) - state = hass.states.get("sensor.cert_expiry_example_com") + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state.state == STATE_UNAVAILABLE with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( @@ -213,12 +184,12 @@ async def test_update_sensor_network_errors(hass): async_fire_time_changed(hass, utcnow() + timedelta(hours=48)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "98" - assert state.attributes.get("error") == "None" - assert state.attributes.get("is_valid") + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == timestamp.isoformat() + assert state.attributes.get("error") == "None" + assert state.attributes.get("is_valid") next_update = starting_time + timedelta(hours=72) @@ -229,13 +200,6 @@ async def test_update_sensor_network_errors(hass): async_fire_time_changed(hass, utcnow() + timedelta(hours=72)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "0" - assert state.attributes.get("error") == "something bad" - assert not state.attributes.get("is_valid") - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is not None assert state.state == STATE_UNKNOWN @@ -250,5 +214,5 @@ async def test_update_sensor_network_errors(hass): async_fire_time_changed(hass, utcnow() + timedelta(hours=96)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_example_com") + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state.state == STATE_UNAVAILABLE From 8dbd54bed156d0b368bed101613ba828021a69a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 13:22:29 +0100 Subject: [PATCH 038/430] Disable parsing scientific/complex number notation in template type (#43170) --- homeassistant/helpers/template.py | 16 +++++++++++++++- tests/helpers/test_template.py | 13 ++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c6efa717fa7..09a9170c79a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -49,6 +49,8 @@ _RENDER_INFO = "template.render_info" _ENVIRONMENT = "template.environment" _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#") +# Match "simple" ints and floats. -1.0, 1, +5, 5.0 +_IS_NUMERIC = re.compile(r"^[+-]?\d*(?:\.\d*)?$") _RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunction"} @@ -373,7 +375,19 @@ class Template: # render, by not returning right here. The evaluation of strings # resulting in strings impacts quotes, to avoid unexpected # output; use the original render instead of the evaluated one. - if not isinstance(result, str): + # Complex and scientific values are also unexpected. Filter them out. + if ( + # Filter out string and complex numbers + not isinstance(result, (str, complex)) + and ( + # Pass if not numeric and not a boolean + not isinstance(result, (int, float)) + # Or it's a boolean (inherit from int) + or isinstance(result, bool) + # Or if it's a digit + or _IS_NUMERIC.match(render_result) is not None + ) + ): return result except (ValueError, TypeError, SyntaxError, MemoryError): pass diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index fe2f23c0033..53186ed35a1 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -346,7 +346,7 @@ def test_tan(hass): (0, 0.0), (math.pi, -0.0), (math.pi / 180 * 45, 1.0), - (math.pi / 180 * 90, 1.633123935319537e16), + (math.pi / 180 * 90, "1.633123935319537e+16"), (math.pi / 180 * 135, -1.0), ("'error'", "error"), ] @@ -2416,5 +2416,16 @@ async def test_parse_result(hass): ('{{ "{{}}" }}', "{{}}"), ("not-something", "not-something"), ("2a", "2a"), + ("123E5", "123E5"), + ("1j", "1j"), + ("1e+100", "1e+100"), + ("0xface", "0xface"), + ("123", 123), + ("123.0", 123.0), + (".5", 0.5), + ("-1", -1), + ("-1.0", -1.0), + ("+1", 1), + ("5.", 5.0), ): assert template.Template(tpl, hass).async_render() == result From 87d86026ed529eb3bcbc7caf8c1fb3c80c171762 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 13 Nov 2020 13:29:57 +0100 Subject: [PATCH 039/430] Fix time_date timestamp offsets (#43165) --- homeassistant/components/time_date/sensor.py | 5 ++++- tests/components/time_date/test_sensor.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 86ff3531240..e8e7d38fe35 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -100,10 +100,13 @@ class TimeDateSensor(Entity): return now + timedelta(seconds=86400) if self.type == "beat": + # Add 1 hour because @0 beats is at 23:00:00 UTC. + timestamp = dt_util.as_timestamp(now + timedelta(hours=1)) interval = 86.4 else: + timestamp = dt_util.as_timestamp(now) interval = 60 - timestamp = int(dt_util.as_timestamp(now)) + delta = interval - (timestamp % interval) next_interval = now + timedelta(seconds=delta) _LOGGER.debug("%s + %s -> %s (%s)", now, delta, next_interval, self.type) diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index bf55ea8075b..8b8695a2879 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -20,16 +20,16 @@ def restore_ts(): async def test_intervals(hass): """Test timing intervals of sensors.""" device = time_date.TimeDateSensor(hass, "time") - now = dt_util.utc_from_timestamp(45) + now = dt_util.utc_from_timestamp(45.5) with patch("homeassistant.util.dt.utcnow", return_value=now): next_time = device.get_next_interval() assert next_time == dt_util.utc_from_timestamp(60) device = time_date.TimeDateSensor(hass, "beat") - now = dt_util.utc_from_timestamp(29) + now = dt_util.parse_datetime("2020-11-13 00:00:29+01:00") with patch("homeassistant.util.dt.utcnow", return_value=now): next_time = device.get_next_interval() - assert next_time == dt_util.utc_from_timestamp(86.4) + assert next_time == dt_util.parse_datetime("2020-11-13 00:01:26.4+01:00") device = time_date.TimeDateSensor(hass, "date_time") now = dt_util.utc_from_timestamp(1495068899) From 0a717f313eaae0a06420fb698891746e03366f0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 13:31:43 +0100 Subject: [PATCH 040/430] Improve entity domain validator (#43164) --- homeassistant/helpers/config_validation.py | 7 +++++-- tests/helpers/test_config_validation.py | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 5b2ad0da2ac..af42b6373ba 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -286,9 +286,12 @@ def entity_domain(domain: Union[str, List[str]]) -> Callable[[Any], str]: """Validate that entity belong to domain.""" ent_domain = entities_domain(domain) - def validate(value: Any) -> str: + def validate(value: str) -> str: """Test if entity domain is domain.""" - return ent_domain(value)[0] + validated = ent_domain(value) + if len(validated) != 1: + raise vol.Invalid(f"Expected exactly 1 entity, got {len(validated)}") + return validated[0] return validate diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index c829d4413f0..7693efbb926 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -179,7 +179,12 @@ def test_entity_domain(): """Test entity domain validation.""" schema = vol.Schema(cv.entity_domain("sensor")) - for value in ("invalid_entity", "cover.demo"): + for value in ( + "invalid_entity", + "cover.demo", + "cover.demo,sensor.another_entity", + "", + ): with pytest.raises(vol.MultipleInvalid): schema(value) From 55b214ec9c1ffaf03f47673797b114e7f53c8b00 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 13 Nov 2020 13:32:56 +0100 Subject: [PATCH 041/430] Fix time_date interval for DST (#43166) --- homeassistant/components/time_date/sensor.py | 4 +-- tests/components/time_date/test_sensor.py | 26 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index e8e7d38fe35..4615e9e046c 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -96,8 +96,8 @@ class TimeDateSensor(Entity): now = dt_util.utcnow() if self.type == "date": - now = dt_util.start_of_local_day(dt_util.as_local(now)) - return now + timedelta(seconds=86400) + tomorrow = dt_util.as_local(now) + timedelta(days=1) + return dt_util.start_of_local_day(tomorrow) if self.type == "beat": # Add 1 hour because @0 beats is at 23:00:00 UTC. diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index 8b8695a2879..b502dad3ea6 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -137,6 +137,32 @@ async def test_timezone_intervals(hass): next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2017-11-14 00:00:00-07:00") + # Entering DST + new_tz = dt_util.get_time_zone("Europe/Prague") + assert new_tz is not None + dt_util.set_default_time_zone(new_tz) + + now = dt_util.parse_datetime("2020-03-29 00:00+01:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() + assert next_time.timestamp() == dt_util.as_timestamp("2020-03-30 00:00+02:00") + + now = dt_util.parse_datetime("2020-03-29 03:00+02:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() + assert next_time.timestamp() == dt_util.as_timestamp("2020-03-30 00:00+02:00") + + # Leaving DST + now = dt_util.parse_datetime("2020-10-25 00:00+02:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() + assert next_time.timestamp() == dt_util.as_timestamp("2020-10-26 00:00:00+01:00") + + now = dt_util.parse_datetime("2020-10-25 23:59+01:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + next_time = device.get_next_interval() + assert next_time.timestamp() == dt_util.as_timestamp("2020-10-26 00:00:00+01:00") + @patch( "homeassistant.util.dt.utcnow", From 7a85b3ea6651c9f0cb5eab468b692e1d0941c83c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 14:20:47 +0100 Subject: [PATCH 042/430] Add test to access current request in websocket API (#43133) --- .../websocket_api/test_decorators.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/components/websocket_api/test_decorators.py diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py new file mode 100644 index 00000000000..45d761f6fed --- /dev/null +++ b/tests/components/websocket_api/test_decorators.py @@ -0,0 +1,68 @@ +"""Test decorators.""" +from homeassistant.components import http, websocket_api + + +async def test_async_response_request_context(hass, websocket_client): + """Test we can access current request.""" + + def handle_request(request, connection, msg): + if request is not None: + connection.send_result(msg["id"], request.path) + else: + connection.send_error(msg["id"], "not_found", "") + + @websocket_api.websocket_command({"type": "test-get-request-executor"}) + @websocket_api.async_response + async def executor_get_request(hass, connection, msg): + handle_request( + await hass.async_add_executor_job(http.current_request.get), connection, msg + ) + + @websocket_api.websocket_command({"type": "test-get-request-async"}) + @websocket_api.async_response + async def async_get_request(hass, connection, msg): + handle_request(http.current_request.get(), connection, msg) + + @websocket_api.websocket_command({"type": "test-get-request"}) + def get_request(hass, connection, msg): + handle_request(http.current_request.get(), connection, msg) + + websocket_api.async_register_command(hass, executor_get_request) + websocket_api.async_register_command(hass, async_get_request) + websocket_api.async_register_command(hass, get_request) + + await websocket_client.send_json( + { + "id": 5, + "type": "test-get-request", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == "/api/websocket" + + await websocket_client.send_json( + { + "id": 6, + "type": "test-get-request-async", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["success"] + assert msg["result"] == "/api/websocket" + + await websocket_client.send_json( + { + "id": 7, + "type": "test-get-request-executor", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert not msg["success"] + assert msg["error"]["code"] == "not_found" From ea550d475a778de5582de71304a7322d159fe6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Fri, 13 Nov 2020 14:35:41 +0100 Subject: [PATCH 043/430] update zigpy_zigate to v0.7.1 (#43159) --- 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 aedb6126559..0a42b6b357e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.11.0", "zigpy==0.27.0", "zigpy-xbee==0.13.0", - "zigpy-zigate==0.7.0", + "zigpy-zigate==0.7.1", "zigpy-znp==0.2.2" ], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index d7991cf8e55..ac5b842d262 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2356,7 +2356,7 @@ zigpy-deconz==0.11.0 zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.7.0 +zigpy-zigate==0.7.1 # homeassistant.components.zha zigpy-znp==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31d24ded554..2a961ec95ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1137,7 +1137,7 @@ zigpy-deconz==0.11.0 zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.7.0 +zigpy-zigate==0.7.1 # homeassistant.components.zha zigpy-znp==0.2.2 From 5ecaa9e4d2788bb197685ac69a2ced838ff1af18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 14:39:50 +0100 Subject: [PATCH 044/430] Add log message server startup (#43177) --- homeassistant/components/http/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 421b17e47cc..9e47dd29a23 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -433,6 +433,8 @@ class HomeAssistantHTTP: "Failed to create HTTP server at port %d: %s", self.server_port, error ) + _LOGGER.info("Now listening on port %d", self.server_port) + async def stop(self): """Stop the aiohttp server.""" await self.site.stop() From 976640102f62def1a239f84f06baf872f4a4796a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Nov 2020 14:42:40 +0100 Subject: [PATCH 045/430] Upgrade sentry-sdk to 0.19.3 (#43176) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 1257c60e75a..c5c75d7f3fe 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.19.2"], + "requirements": ["sentry-sdk==0.19.3"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index ac5b842d262..e83f79096a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2006,7 +2006,7 @@ sense-hat==2.2.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.2 +sentry-sdk==0.19.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a961ec95ff..987066ee41b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -969,7 +969,7 @@ samsungtvws==1.4.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.2 +sentry-sdk==0.19.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 5de30e19bb7c95a0ea915914fe4f99aa0d5c68cc Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 13 Nov 2020 15:40:46 +0100 Subject: [PATCH 046/430] Rewrite google_wifi unittest tests to pytest style (#42030) --- tests/components/google_wifi/test_sensor.py | 329 ++++++++++---------- 1 file changed, 157 insertions(+), 172 deletions(-) diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index ff3ec0429fa..a9e09c8b66d 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,17 +1,13 @@ """The tests for the Google Wifi platform.""" from datetime import datetime, timedelta -import unittest -import requests_mock - -from homeassistant import core as ha import homeassistant.components.google_wifi.sensor as google_wifi from homeassistant.const import STATE_UNKNOWN -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.async_mock import Mock, patch -from tests.common import assert_setup_component, get_test_home_assistant +from tests.common import assert_setup_component, async_fire_time_changed NAME = "foo" @@ -34,185 +30,174 @@ MOCK_DATA_NEXT = ( MOCK_DATA_MISSING = '{"software": {},' '"system": {},' '"wan": {}}' -class TestGoogleWifiSetup(unittest.TestCase): - """Tests for setting up the Google Wifi sensor platform.""" - - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.hass.stop) - - @requests_mock.Mocker() - def test_setup_minimum(self, mock_req): - """Test setup with minimum configuration.""" - resource = f"http://{google_wifi.DEFAULT_HOST}{google_wifi.ENDPOINT}" - mock_req.get(resource, status_code=200) - assert setup_component( - self.hass, - "sensor", - {"sensor": {"platform": "google_wifi", "monitored_conditions": ["uptime"]}}, - ) - assert_setup_component(1, "sensor") - - @requests_mock.Mocker() - def test_setup_get(self, mock_req): - """Test setup with full configuration.""" - resource = f"http://localhost{google_wifi.ENDPOINT}" - mock_req.get(resource, status_code=200) - assert setup_component( - self.hass, - "sensor", - { - "sensor": { - "platform": "google_wifi", - "host": "localhost", - "name": "Test Wifi", - "monitored_conditions": [ - "current_version", - "new_version", - "uptime", - "last_restart", - "local_ip", - "status", - ], - } - }, - ) - assert_setup_component(6, "sensor") +async def test_setup_minimum(hass, requests_mock): + """Test setup with minimum configuration.""" + resource = f"http://{google_wifi.DEFAULT_HOST}{google_wifi.ENDPOINT}" + requests_mock.get(resource, status_code=200) + assert await async_setup_component( + hass, + "sensor", + {"sensor": {"platform": "google_wifi", "monitored_conditions": ["uptime"]}}, + ) + assert_setup_component(1, "sensor") -class TestGoogleWifiSensor(unittest.TestCase): - """Tests for Google Wifi sensor platform.""" - - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - with requests_mock.Mocker() as mock_req: - self.setup_api(MOCK_DATA, mock_req) - self.addCleanup(self.hass.stop) - - def setup_api(self, data, mock_req): - """Set up API with fake data.""" - resource = f"http://localhost{google_wifi.ENDPOINT}" - now = datetime(1970, month=1, day=1) - with patch("homeassistant.util.dt.now", return_value=now): - mock_req.get(resource, text=data, status_code=200) - conditions = google_wifi.MONITORED_CONDITIONS.keys() - self.api = google_wifi.GoogleWifiAPI("localhost", conditions) - self.name = NAME - self.sensor_dict = {} - for condition, cond_list in google_wifi.MONITORED_CONDITIONS.items(): - sensor = google_wifi.GoogleWifiSensor(self.api, self.name, condition) - name = f"{self.name}_{condition}" - units = cond_list[1] - icon = cond_list[2] - self.sensor_dict[condition] = { - "sensor": sensor, - "name": name, - "units": units, - "icon": icon, +async def test_setup_get(hass, requests_mock): + """Test setup with full configuration.""" + resource = f"http://localhost{google_wifi.ENDPOINT}" + requests_mock.get(resource, status_code=200) + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "google_wifi", + "host": "localhost", + "name": "Test Wifi", + "monitored_conditions": [ + "current_version", + "new_version", + "uptime", + "last_restart", + "local_ip", + "status", + ], } + }, + ) + assert_setup_component(6, "sensor") - def fake_delay(self, ha_delay): - """Fake delay to prevent update throttle.""" - hass_now = dt_util.utcnow() - shifted_time = hass_now + timedelta(seconds=ha_delay) - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time}) - def test_name(self): - """Test the name.""" - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - test_name = self.sensor_dict[name]["name"] - assert test_name == sensor.name +def setup_api(data, requests_mock): + """Set up API with fake data.""" + resource = f"http://localhost{google_wifi.ENDPOINT}" + now = datetime(1970, month=1, day=1) + sensor_dict = {} + with patch("homeassistant.util.dt.now", return_value=now): + requests_mock.get(resource, text=data, status_code=200) + conditions = google_wifi.MONITORED_CONDITIONS.keys() + api = google_wifi.GoogleWifiAPI("localhost", conditions) + for condition, cond_list in google_wifi.MONITORED_CONDITIONS.items(): + sensor_dict[condition] = { + "sensor": google_wifi.GoogleWifiSensor(api, NAME, condition), + "name": f"{NAME}_{condition}", + "units": cond_list[1], + "icon": cond_list[2], + } + return api, sensor_dict - def test_unit_of_measurement(self): - """Test the unit of measurement.""" - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - assert self.sensor_dict[name]["units"] == sensor.unit_of_measurement - def test_icon(self): - """Test the icon.""" - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - assert self.sensor_dict[name]["icon"] == sensor.icon +def fake_delay(hass, ha_delay): + """Fake delay to prevent update throttle.""" + hass_now = dt_util.utcnow() + shifted_time = hass_now + timedelta(seconds=ha_delay) + async_fire_time_changed(hass, shifted_time) - @requests_mock.Mocker() - def test_state(self, mock_req): - """Test the initial state.""" - self.setup_api(MOCK_DATA, mock_req) - now = datetime(1970, month=1, day=1) - with patch("homeassistant.util.dt.now", return_value=now): - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - self.fake_delay(2) - sensor.update() - if name == google_wifi.ATTR_LAST_RESTART: - assert "1969-12-31 00:00:00" == sensor.state - elif name == google_wifi.ATTR_UPTIME: - assert 1 == sensor.state - elif name == google_wifi.ATTR_STATUS: - assert "Online" == sensor.state - else: - assert "initial" == sensor.state - @requests_mock.Mocker() - def test_update_when_value_is_none(self, mock_req): - """Test state gets updated to unknown when sensor returns no data.""" - self.setup_api(None, mock_req) - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - self.fake_delay(2) +def test_name(requests_mock): + """Test the name.""" + api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + test_name = sensor_dict[name]["name"] + assert test_name == sensor.name + + +def test_unit_of_measurement(requests_mock): + """Test the unit of measurement.""" + api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + assert sensor_dict[name]["units"] == sensor.unit_of_measurement + + +def test_icon(requests_mock): + """Test the icon.""" + api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + assert sensor_dict[name]["icon"] == sensor.icon + + +def test_state(hass, requests_mock): + """Test the initial state.""" + api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + now = datetime(1970, month=1, day=1) + with patch("homeassistant.util.dt.now", return_value=now): + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + fake_delay(hass, 2) sensor.update() - assert sensor.state is None + if name == google_wifi.ATTR_LAST_RESTART: + assert "1969-12-31 00:00:00" == sensor.state + elif name == google_wifi.ATTR_UPTIME: + assert 1 == sensor.state + elif name == google_wifi.ATTR_STATUS: + assert "Online" == sensor.state + else: + assert "initial" == sensor.state - @requests_mock.Mocker() - def test_update_when_value_changed(self, mock_req): - """Test state gets updated when sensor returns a new status.""" - self.setup_api(MOCK_DATA_NEXT, mock_req) - now = datetime(1970, month=1, day=1) - with patch("homeassistant.util.dt.now", return_value=now): - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - self.fake_delay(2) - sensor.update() - if name == google_wifi.ATTR_LAST_RESTART: - assert "1969-12-30 00:00:00" == sensor.state - elif name == google_wifi.ATTR_UPTIME: - assert 2 == sensor.state - elif name == google_wifi.ATTR_STATUS: - assert "Offline" == sensor.state - elif name == google_wifi.ATTR_NEW_VERSION: - assert "Latest" == sensor.state - elif name == google_wifi.ATTR_LOCAL_IP: - assert STATE_UNKNOWN == sensor.state - else: - assert "next" == sensor.state - @requests_mock.Mocker() - def test_when_api_data_missing(self, mock_req): - """Test state logs an error when data is missing.""" - self.setup_api(MOCK_DATA_MISSING, mock_req) - now = datetime(1970, month=1, day=1) - with patch("homeassistant.util.dt.now", return_value=now): - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] - self.fake_delay(2) - sensor.update() +def test_update_when_value_is_none(hass, requests_mock): + """Test state gets updated to unknown when sensor returns no data.""" + api, sensor_dict = setup_api(None, requests_mock) + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + fake_delay(hass, 2) + sensor.update() + assert sensor.state is None + + +def test_update_when_value_changed(hass, requests_mock): + """Test state gets updated when sensor returns a new status.""" + api, sensor_dict = setup_api(MOCK_DATA_NEXT, requests_mock) + now = datetime(1970, month=1, day=1) + with patch("homeassistant.util.dt.now", return_value=now): + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + fake_delay(hass, 2) + sensor.update() + if name == google_wifi.ATTR_LAST_RESTART: + assert "1969-12-30 00:00:00" == sensor.state + elif name == google_wifi.ATTR_UPTIME: + assert 2 == sensor.state + elif name == google_wifi.ATTR_STATUS: + assert "Offline" == sensor.state + elif name == google_wifi.ATTR_NEW_VERSION: + assert "Latest" == sensor.state + elif name == google_wifi.ATTR_LOCAL_IP: assert STATE_UNKNOWN == sensor.state + else: + assert "next" == sensor.state - def test_update_when_unavailable(self): - """Test state updates when Google Wifi unavailable.""" - self.api.update = Mock( - "google_wifi.GoogleWifiAPI.update", side_effect=self.update_side_effect() - ) - for name in self.sensor_dict: - sensor = self.sensor_dict[name]["sensor"] + +def test_when_api_data_missing(hass, requests_mock): + """Test state logs an error when data is missing.""" + api, sensor_dict = setup_api(MOCK_DATA_MISSING, requests_mock) + now = datetime(1970, month=1, day=1) + with patch("homeassistant.util.dt.now", return_value=now): + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + fake_delay(hass, 2) sensor.update() - assert sensor.state is None + assert STATE_UNKNOWN == sensor.state - def update_side_effect(self): - """Mock representation of update function.""" - self.api.data = None - self.api.available = False + +def test_update_when_unavailable(requests_mock): + """Test state updates when Google Wifi unavailable.""" + api, sensor_dict = setup_api(None, requests_mock) + api.update = Mock( + "google_wifi.GoogleWifiAPI.update", + side_effect=update_side_effect(requests_mock), + ) + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + sensor.update() + assert sensor.state is None + + +def update_side_effect(requests_mock): + """Mock representation of update function.""" + api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api.data = None + api.available = False From 36b4a8e128f617f2da005e60b5938bb3d0d033fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 15:52:15 +0100 Subject: [PATCH 047/430] Revert opensky flight latitude and longitude (#43185) This reverts commit 0f46916f9efec5832423ea7db8e477e34c1038d6. --- homeassistant/components/opensky/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index c9a863f86ab..49edf8e7d0a 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -125,8 +125,6 @@ class OpenSkySensor(Entity): ATTR_CALLSIGN: flight, ATTR_ALTITUDE: altitude, ATTR_SENSOR: self._name, - ATTR_LONGITUDE: flight.get(ATTR_LONGITUDE), - ATTR_LATITUDE: flight.get(ATTR_LATITUDE), } self._hass.bus.fire(event, data) From 37361a6332881cacc0ca83eb920c2a55c9ab74f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 16:03:08 +0100 Subject: [PATCH 048/430] Prevent spider from doing I/O in the event loop (#43182) --- homeassistant/components/spider/__init__.py | 27 ++++++++++----------- homeassistant/components/spider/climate.py | 9 ++++--- homeassistant/components/spider/switch.py | 10 +++++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index f2e9a06fb94..b0c34ae5a08 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -2,11 +2,12 @@ import asyncio import logging -from spiderpy.spiderapi import SpiderApi, UnauthorizedException +from spiderpy.spiderapi import SpiderApi, SpiderApiException, UnauthorizedException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS @@ -29,16 +30,6 @@ CONFIG_SCHEMA = vol.Schema( ) -def _spider_startup_wrapper(entry): - """Startup wrapper for spider.""" - api = SpiderApi( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - entry.data[CONF_SCAN_INTERVAL], - ) - return api - - async def async_setup(hass, config): """Set up a config entry.""" hass.data[DOMAIN] = {} @@ -60,12 +51,20 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Spider via config entry.""" try: - hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( - _spider_startup_wrapper, entry + api = await hass.async_add_executor_job( + SpiderApi, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_SCAN_INTERVAL], ) except UnauthorizedException: - _LOGGER.error("Can't connect to the Spider API") + _LOGGER.error("Authorization failed") return False + except SpiderApiException as err: + _LOGGER.error("Can't connect to the Spider API: %s", err) + raise ConfigEntryNotReady from err + + hass.data[DOMAIN][entry.entry_id] = api for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 234ae699bca..7730d8b34c4 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -28,9 +28,12 @@ async def async_setup_entry(hass, config, async_add_entities): """Initialize a Spider thermostat.""" api = hass.data[DOMAIN][config.entry_id] - entities = [SpiderThermostat(api, entity) for entity in api.get_thermostats()] - - async_add_entities(entities) + async_add_entities( + [ + SpiderThermostat(api, entity) + for entity in await hass.async_add_executor_job(api.get_thermostats) + ] + ) class SpiderThermostat(ClimateEntity): diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 62f220bf805..1b0c86468ea 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -7,10 +7,12 @@ from .const import DOMAIN async def async_setup_entry(hass, config, async_add_entities): """Initialize a Spider thermostat.""" api = hass.data[DOMAIN][config.entry_id] - - entities = [SpiderPowerPlug(api, entity) for entity in api.get_power_plugs()] - - async_add_entities(entities) + async_add_entities( + [ + SpiderPowerPlug(api, entity) + for entity in await hass.async_add_executor_job(api.get_power_plugs) + ] + ) class SpiderPowerPlug(SwitchEntity): From 6daf40b25413dfa606c007aef16307eb0d656758 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 13 Nov 2020 16:03:40 +0100 Subject: [PATCH 049/430] Copy default vscode settings during bootstrap (#43180) --- .devcontainer/devcontainer.json | 4 ++++ .pre-commit-config.yaml | 2 +- .vscode/{settings.json => settings.default.json} | 7 ++++--- script/bootstrap | 8 ++++++++ 4 files changed, 17 insertions(+), 4 deletions(-) rename .vscode/{settings.json => settings.default.json} (58%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6c17601b98a..3dc26ed2a94 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,10 +12,14 @@ "redhat.vscode-yaml", "esbenp.prettier-vscode" ], + // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json "settings": { "python.pythonPath": "/usr/local/bin/python", "python.linting.pylintEnabled": true, "python.linting.enabled": true, + "python.formatting.provider": "black", + "python.testing.pytestArgs": ["--no-cov"], + "python.testing.pytestEnabled": true, "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2fbe9761d0..9a69f0b444c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: check-executables-have-shebangs stages: [manual] - id: check-json - exclude: .vscode + exclude: (.vscode|.devcontainer) - id: no-commit-to-branch args: - --branch=dev diff --git a/.vscode/settings.json b/.vscode/settings.default.json similarity index 58% rename from .vscode/settings.json rename to .vscode/settings.default.json index 910db092e70..85cf4e8b83a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.default.json @@ -1,8 +1,9 @@ { + // Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json "python.formatting.provider": "black", - // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings - "python.testing.pytestEnabled": true, // Added --no-cov to work around TypeError: message must be set // https://github.com/microsoft/vscode-python/issues/14067 - "python.testing.pytestArgs": ["--no-cov"] + "python.testing.pytestArgs": ["--no-cov"], + // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings + "python.testing.pytestEnabled": true } diff --git a/script/bootstrap b/script/bootstrap index 3166b8c7701..f58268ff1a8 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -6,6 +6,14 @@ set -e cd "$(dirname "$0")/.." +# Add default vscode settings if not existing +SETTINGS_FILE=./.vscode/settings.json +SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json +if [ ! -f "$SETTINGS_FILE" ]; then + echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE." + cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE" +fi + echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) --constraint homeassistant/package_constraints.txt From 71b8aad91b523133bfbe926785fcc0494ec8a795 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 13 Nov 2020 10:06:34 -0500 Subject: [PATCH 050/430] Update ozw get_config_parameter websocket response (#43058) --- homeassistant/components/ozw/websocket_api.py | 59 ++++++++++++++++++- tests/components/ozw/test_websocket_api.py | 14 +++-- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index 482d78bb878..ea6df900907 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -15,6 +15,7 @@ from openzwavemqtt.util.node import ( set_config_parameter, ) import voluptuous as vol +import voluptuous_serialize from homeassistant.components import websocket_api from homeassistant.core import callback @@ -29,6 +30,7 @@ OZW_INSTANCE = "ozw_instance" NODE_ID = "node_id" PARAMETER = ATTR_CONFIG_PARAMETER VALUE = ATTR_CONFIG_VALUE +SCHEMA = "schema" ATTR_NODE_QUERY_STAGE = "node_query_stage" ATTR_IS_ZWAVE_PLUS = "is_zwave_plus" @@ -106,6 +108,59 @@ def _call_util_function(hass, connection, msg, send_result, function, *args): connection.send_result(msg[ID]) +def _get_config_params(node, *args): + raw_values = get_config_parameters(node) + config_params = [] + + for param in raw_values: + schema = {} + + if param["type"] in ["Byte", "Int", "Short"]: + schema = vol.Schema( + { + vol.Required(param["label"], default=param["value"]): vol.All( + vol.Coerce(int), vol.Range(min=param["min"], max=param["max"]) + ) + } + ) + data = {param["label"]: param["value"]} + + if param["type"] == "List": + + for options in param["options"]: + if options["Label"] == param["value"]: + selected = options + break + + schema = vol.Schema( + { + vol.Required(param["label"],): vol.In( + { + option["Value"]: option["Label"] + for option in param["options"] + } + ) + } + ) + data = {param["label"]: selected["Value"]} + + config_params.append( + { + "type": param["type"], + "label": param["label"], + "parameter": param["parameter"], + "help": param["help"], + "value": param["value"], + "schema": voluptuous_serialize.convert( + schema, custom_serializer=cv.custom_serializer + ), + "data": data, + } + ) + + return config_params + + @websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"}) def websocket_get_instances(hass, connection, msg): """Get a list of OZW instances.""" @@ -213,7 +268,7 @@ def websocket_get_code_slots(hass, connection, msg): ) def websocket_get_config_parameters(hass, connection, msg): """Get a list of configuration parameters for an OZW node instance.""" - _call_util_function(hass, connection, msg, True, get_config_parameters) + _call_util_function(hass, connection, msg, True, _get_config_params) @websocket_api.websocket_command( @@ -245,7 +300,7 @@ def websocket_get_config_parameters(hass, connection, msg): def websocket_set_config_parameter(hass, connection, msg): """Set a config parameter to a node.""" _call_util_function( - hass, connection, msg, False, set_config_parameter, msg[PARAMETER], msg[VALUE] + hass, connection, msg, True, set_config_parameter, msg[PARAMETER], msg[VALUE] ) diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index d4194b0a537..37c6d184c8e 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -28,6 +28,7 @@ from homeassistant.components.ozw.websocket_api import ( NODE_ID, OZW_INSTANCE, PARAMETER, + SCHEMA, TYPE, VALUE, ) @@ -152,16 +153,17 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): # Test set config parameter config_param = result[0] + print(config_param) current_val = config_param[ATTR_VALUE] new_val = next( - option["Value"] - for option in config_param[ATTR_OPTIONS] - if option["Label"] != current_val + option[0] + for option in config_param[SCHEMA][0][ATTR_OPTIONS] + if option[0] != current_val ) new_label = next( - option["Label"] - for option in config_param[ATTR_OPTIONS] - if option["Label"] != current_val and option["Value"] != new_val + option[1] + for option in config_param[SCHEMA][0][ATTR_OPTIONS] + if option[1] != current_val and option[0] != new_val ) await client.send_json( { From 6e7b14f13952fcee0fc0d21a9bb11dc4fb0282f9 Mon Sep 17 00:00:00 2001 From: Thomas <71447672+ttuffin@users.noreply.github.com> Date: Fri, 13 Nov 2020 17:51:33 +0100 Subject: [PATCH 051/430] Hide cancelled trips from being displayed in vasttrafik integration (#43184) --- homeassistant/components/vasttrafik/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index d76fc3b5e3d..2b904faab4d 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -132,6 +132,8 @@ class VasttrafikDepartureSensor(Entity): else: for departure in self._departureboard: line = departure.get("sname") + if "cancelled" in departure: + continue if not self._lines or line in self._lines: if "rtTime" in departure: self._state = departure["rtTime"] From df25b53bb8104d0b99acbb9eecc9b8d29b684e23 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 22:49:01 +0100 Subject: [PATCH 052/430] Fix automation in packages (#43202) --- homeassistant/components/automation/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5056f225251..74a319242ec 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -52,6 +52,8 @@ from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime +# Not used except by packages to check config structure +from .config import PLATFORM_SCHEMA # noqa from .config import async_validate_config_item from .const import ( CONF_ACTION, From bae026a6fef3c23011f7019f385fc571dc0e4a2b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Nov 2020 22:53:55 +0100 Subject: [PATCH 053/430] Add support for selectors in services.yaml (#43162) * Add support for selectors in services.yaml * Add base schema validation --- homeassistant/components/blueprint/schemas.py | 5 +- homeassistant/components/sonos/services.yaml | 36 ++++++++++++ homeassistant/const.py | 1 + homeassistant/helpers/selector.py | 57 +++++++++++++++++++ script/hassfest/services.py | 4 +- tests/helpers/test_selector.py | 44 ++++++++++++++ 6 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 homeassistant/helpers/selector.py create mode 100644 tests/helpers/test_selector.py diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index e04bc99e4b7..b734476b85a 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -3,9 +3,9 @@ from typing import Any import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH +from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, CONF_SELECTOR from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, selector from .const import ( CONF_BLUEPRINT, @@ -32,6 +32,7 @@ BLUEPRINT_INPUT_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): str, vol.Optional(CONF_DESCRIPTION): str, + vol.Optional(CONF_SELECTOR): selector.validate_selector, } ) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index effb19c47b7..8a35e9a7790 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -7,6 +7,10 @@ join: entity_id: description: Name(s) of entities that will join the master. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player unjoin: description: Unjoin the player from a group. @@ -14,6 +18,10 @@ unjoin: entity_id: description: Name(s) of entities that will be unjoined from their group. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player snapshot: description: Take a snapshot of the media player. @@ -21,6 +29,10 @@ snapshot: entity_id: description: Name(s) of entities that will be snapshot. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player with_group: description: True (default) or False. Also snapshot the group layout. example: "true" @@ -31,6 +43,10 @@ restore: entity_id: description: Name(s) of entities that will be restored. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player with_group: description: True (default) or False. Also restore the group layout. example: "true" @@ -41,6 +57,10 @@ set_sleep_timer: entity_id: description: Name(s) of entities that will have a timer set. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player sleep_time: description: Number of seconds to set the timer. example: "900" @@ -51,6 +71,10 @@ clear_sleep_timer: entity_id: description: Name(s) of entities that will have the timer cleared. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player set_option: description: Set Sonos sound options. @@ -58,6 +82,10 @@ set_option: entity_id: description: Name(s) of entities that will have options set. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player night_sound: description: Enable Night Sound mode example: "true" @@ -74,6 +102,10 @@ play_queue: entity_id: description: Name(s) of entities that will start playing. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player queue_position: description: Position of the song in the queue to start playing from. example: "0" @@ -84,6 +116,10 @@ remove_from_queue: entity_id: description: Name(s) of entities that will remove an item. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player queue_position: description: Position in the queue to remove. example: "0" diff --git a/homeassistant/const.py b/homeassistant/const.py index 84a24ab1bad..d60e443818e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -154,6 +154,7 @@ CONF_RGB = "rgb" CONF_ROOM = "room" CONF_SCAN_INTERVAL = "scan_interval" CONF_SCENE = "scene" +CONF_SELECTOR = "selector" CONF_SENDER = "sender" CONF_SENSORS = "sensors" CONF_SENSOR_TYPE = "sensor_type" diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py new file mode 100644 index 00000000000..9f049e07213 --- /dev/null +++ b/homeassistant/helpers/selector.py @@ -0,0 +1,57 @@ +"""Selectors for Home Assistant.""" +from typing import Any, Callable, Dict, cast + +import voluptuous as vol + +from homeassistant.util import decorator + +SELECTORS = decorator.Registry() + + +def validate_selector(config: Any) -> Dict: + """Validate a selector.""" + if not isinstance(config, dict): + raise vol.Invalid("Expected a dictionary") + + if len(config) != 1: + raise vol.Invalid(f"Only one type can be specified. Found {', '.join(config)}") + + selector_type = list(config)[0] + + seslector_class = SELECTORS.get(selector_type) + + if seslector_class is None: + raise vol.Invalid(f"Unknown selector type {selector_type} found") + + return cast(Dict, seslector_class.CONFIG_SCHEMA(config[selector_type])) + + +class Selector: + """Base class for selectors.""" + + CONFIG_SCHEMA: Callable + + +@SELECTORS.register("entity") +class EntitySelector(Selector): + """Selector of a single entity.""" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("integration"): str, + vol.Optional("domain"): str, + } + ) + + +@SELECTORS.register("device") +class DeviceSelector(Selector): + """Selector of a single device.""" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("integration"): str, + vol.Optional("manufacturer"): str, + vol.Optional("model"): str, + } + ) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 1e05ef63efb..c07d3bbc6ef 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -6,8 +6,9 @@ from typing import Dict import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.const import CONF_SELECTOR from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, selector from homeassistant.util.yaml import load_yaml from .model import Integration @@ -27,6 +28,7 @@ FIELD_SCHEMA = vol.Schema( vol.Optional("default"): exists, vol.Optional("values"): exists, vol.Optional("required"): bool, + vol.Optional(CONF_SELECTOR): selector.validate_selector, } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py new file mode 100644 index 00000000000..2b6b5cbc9f8 --- /dev/null +++ b/tests/helpers/test_selector.py @@ -0,0 +1,44 @@ +"""Test selectors.""" +import pytest +import voluptuous as vol + +from homeassistant.helpers import selector + + +@pytest.mark.parametrize( + "schema", ({}, {"non_existing": {}}, {"device": {}, "entity": {}}) +) +def test_invalid_base_schema(schema): + """Test base schema validation.""" + with pytest.raises(vol.Invalid): + selector.validate_selector(schema) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"integration": "zha"}, + {"manufacturer": "mock-manuf"}, + {"model": "mock-model"}, + {"manufacturer": "mock-manuf", "model": "mock-model"}, + {"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"}, + ), +) +def test_device_selector_schema(schema): + """Test device selector.""" + selector.validate_selector({"device": schema}) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"integration": "zha"}, + {"domain": "light"}, + {"integration": "zha", "domain": "light"}, + ), +) +def test_entity_selector_schema(schema): + """Test device selector.""" + selector.validate_selector({"entity": schema}) From 16364636d1e48fe0080767241849b274ec3998f8 Mon Sep 17 00:00:00 2001 From: czechmark Date: Fri, 13 Nov 2020 17:10:04 -0500 Subject: [PATCH 054/430] Update fitbit battery status (#42980) * Read the device_status in order to update the fitbit battery * Read the device_status in order to update the fitbit battery --- homeassistant/components/fitbit/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index f6e3fd90fe5..387eb78448c 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -472,7 +472,13 @@ class FitbitSensor(Entity): def update(self): """Get the latest data from the Fitbit API and update the states.""" if self.resource_type == "devices/battery" and self.extra: + registered_devs = self.client.get_devices() + device_id = self.extra.get("id") + self.extra = list( + filter(lambda device: device.get("id") == device_id, registered_devs) + )[0] self._state = self.extra.get("battery") + else: container = self.resource_type.replace("/", "-") response = self.client.time_series(self.resource_type, period="7d") From 907907e6f9ff2ff2c94a94ee39ff7a41c7e26f1b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 13 Nov 2020 16:04:34 -0700 Subject: [PATCH 055/430] Revert "Remove YAML config for Tile (#43064)" (#43199) This reverts commit 19f48e180c7e6a34bd93d7a849147a8de491771c. --- homeassistant/components/tile/config_flow.py | 28 +++++++++----- .../components/tile/device_tracker.py | 26 ++++++++++++- tests/components/tile/test_config_flow.py | 38 +++++++++++++++++-- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 932c948defe..87f58193e9d 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -9,10 +9,6 @@ from homeassistant.helpers import aiohttp_client from .const import DOMAIN # pylint: disable=unused-import -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} -) - class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Tile config flow.""" @@ -20,10 +16,26 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the config flow.""" + self.data_schema = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ) + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", data_schema=self.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 self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + return await self._show_form() await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() @@ -35,10 +47,6 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session ) except TileError: - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": "invalid_auth"}, - ) + return await self._show_form({"base": "invalid_auth"}) return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 286ffa98869..5b0065b2c4e 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -3,6 +3,8 @@ import logging from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from . import DATA_COORDINATOR, DOMAIN, TileEntity @@ -26,10 +28,32 @@ async def async_setup_entry(hass, config_entry, async_add_entities): [ TileDeviceTracker(coordinator, tile_uuid, tile) for tile_uuid, tile in coordinator.data.items() - ] + ], + True, ) +async def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Detect a legacy configuration and import it.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + }, + ) + ) + + _LOGGER.info( + "Your Tile configuration has been imported into the UI; " + "please remove it from configuration.yaml" + ) + + return True + + class TileDeviceTracker(TileEntity, TrackerEntity): """Representation of a network infrastructure device.""" diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index edb95cc5b05..b9d4799dc2f 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -3,7 +3,7 @@ from pytile.errors import TileError from homeassistant import data_entry_flow from homeassistant.components.tile import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.async_mock import patch @@ -12,7 +12,10 @@ from tests.common import MockConfigEntry async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" - conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "123abc"} + conf = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } MockConfigEntry(domain=DOMAIN, unique_id="user@host.com", data=conf).add_to_hass( hass @@ -28,7 +31,10 @@ async def test_duplicate_error(hass): async def test_invalid_credentials(hass): """Test that invalid credentials key throws an error.""" - conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "123abc"} + conf = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } with patch( "homeassistant.components.tile.config_flow.async_login", @@ -41,9 +47,33 @@ async def test_invalid_credentials(hass): assert result["errors"] == {"base": "invalid_auth"} +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } + + with patch( + "homeassistant.components.tile.async_setup_entry", return_value=True + ), patch("homeassistant.components.tile.config_flow.async_login"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@host.com" + assert result["data"] == { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } + + async def test_step_user(hass): """Test that the user step works.""" - conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "123abc"} + conf = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } with patch( "homeassistant.components.tile.async_setup_entry", return_value=True From d7f1deeb00bd748c2646d0c36c247ac4c6191761 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 14 Nov 2020 00:07:02 +0000 Subject: [PATCH 056/430] [ci skip] Translation update --- .../aurora/translations/zh-Hant.json | 26 +++++++++++++++++++ .../components/dsmr/translations/zh-Hant.json | 10 +++++++ .../components/hassio/translations/ru.json | 7 +++-- .../hassio/translations/zh-Hant.json | 16 ++++++++++++ .../homeassistant/translations/ca.json | 1 + .../homeassistant/translations/cs.json | 1 + .../homeassistant/translations/et.json | 1 + .../homeassistant/translations/no.json | 1 + .../homeassistant/translations/pl.json | 3 ++- .../homeassistant/translations/ru.json | 1 + .../homeassistant/translations/zh-Hant.json | 3 ++- .../lovelace/translations/zh-Hant.json | 3 ++- .../recollect_waste/translations/zh-Hant.json | 18 +++++++++++++ 13 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/aurora/translations/zh-Hant.json create mode 100644 homeassistant/components/recollect_waste/translations/zh-Hant.json diff --git a/homeassistant/components/aurora/translations/zh-Hant.json b/homeassistant/components/aurora/translations/zh-Hant.json new file mode 100644 index 00000000000..e1824a7ff4a --- /dev/null +++ b/homeassistant/components/aurora/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u95a5\u503c (%)" + } + } + } + }, + "title": "NOAA Aurora \u50b3\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index 1ab3e1f720f..e35c96a7bf7 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u5be6\u9ad4\u66f4\u65b0\u9593\u9694\u6700\u5c0f\u6642\u9593" + }, + "title": "DSMR \u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 052e1ec21dd..4f9b16621fc 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -2,13 +2,16 @@ "system_health": { "info": { "board": "\u041f\u043b\u0430\u0442\u0430", + "disk_total": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", + "disk_used": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e", "docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f Docker", + "healthy": "\u0418\u0441\u043f\u0440\u0430\u0432\u043d\u043e", "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0445\u043e\u0441\u0442\u0430", "installed_addons": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f", "supervisor_api": "Supervisor API", "supervisor_version": "\u0412\u0435\u0440\u0441\u0438\u044f Supervisor", - "supported": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", - "update_channel": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u0430\u043d\u0430\u043b", + "supported": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430", + "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439", "version_api": "\u0412\u0435\u0440\u0441\u0438\u044f API" } }, diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index 981cb51c83a..574b82358d6 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "\u677f", + "disk_total": "\u7e3d\u78c1\u789f\u7a7a\u9593", + "disk_used": "\u5df2\u4f7f\u7528\u7a7a\u9593", + "docker_version": "Docker \u7248\u672c", + "healthy": "\u5065\u5eb7\u5ea6", + "host_os": "\u4e3b\u6a5f\u4f5c\u696d\u7cfb\u7d71", + "installed_addons": "\u5df2\u5b89\u88dd Add-on", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor \u7248\u672c", + "supported": "\u652f\u63f4", + "update_channel": "\u66f4\u65b0\u983b\u9053", + "version_api": "\u7248\u672c API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index f02939b201e..8f9931c6820 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -6,6 +6,7 @@ "dev": "Desenvolupament", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Tipus d'instal\u00b7laci\u00f3", "os_name": "Fam\u00edlia del sistema operatiu", diff --git a/homeassistant/components/homeassistant/translations/cs.json b/homeassistant/components/homeassistant/translations/cs.json index 46bf2c56b4b..3b6414b58ad 100644 --- a/homeassistant/components/homeassistant/translations/cs.json +++ b/homeassistant/components/homeassistant/translations/cs.json @@ -6,6 +6,7 @@ "dev": "V\u00fdvoj", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Typ instalace", "os_name": "Rodina opera\u010dn\u00edch syst\u00e9m\u016f", diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 7bdecd0178f..22e3ab1e00d 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -6,6 +6,7 @@ "dev": "Arendus", "docker": "Docker", "docker_version": "Docker", + "hassio": "Haldur", "host_os": "Home Assistant OS", "installation_type": "Paigalduse t\u00fc\u00fcp", "os_name": "Operatsioonis\u00fcsteemi j\u00e4rk", diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 72bba59116c..3cf39e2cf7d 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -6,6 +6,7 @@ "dev": "Utvikling", "docker": "", "docker_version": "", + "hassio": "Supervisor", "host_os": "", "installation_type": "Installasjonstype", "os_name": "Familie for operativsystem", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index e65756cfc83..44d35cb582c 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -3,9 +3,10 @@ "info": { "arch": "Architektura procesora", "chassis": "Wersja komputera", - "dev": "Wersja rozwojowa", + "dev": "Wersja deweloperska", "docker": "Docker", "docker_version": "Wersja Dockera", + "hassio": "Supervisor", "host_os": "System operacyjny HA", "installation_type": "Typ instalacji", "os_name": "Rodzina systemu operacyjnego", diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index 76af5da7d13..651400c5fe5 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -6,6 +6,7 @@ "dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", "os_name": "\u0421\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c", diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index 2bc13851390..eba7a8034db 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -6,9 +6,10 @@ "dev": "\u958b\u767c\u7248", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "\u5b89\u88dd\u985e\u578b", - "os_name": "\u4f5c\u696d\u7cfb\u7d71\u540d\u7a31", + "os_name": "\u4f5c\u696d\u7cfb\u7d71\u5bb6\u65cf", "os_version": "\u4f5c\u696d\u7cfb\u7d71\u7248\u672c", "python_version": "Python \u7248\u672c", "supervisor": "Supervisor", diff --git a/homeassistant/components/lovelace/translations/zh-Hant.json b/homeassistant/components/lovelace/translations/zh-Hant.json index b1f4309bd42..0f8ec76e3a3 100644 --- a/homeassistant/components/lovelace/translations/zh-Hant.json +++ b/homeassistant/components/lovelace/translations/zh-Hant.json @@ -3,7 +3,8 @@ "info": { "dashboards": "\u4e3b\u9762\u677f", "mode": "\u6a21\u5f0f", - "resources": "\u8cc7\u6e90" + "resources": "\u8cc7\u6e90", + "views": "\u9762\u677f" } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json new file mode 100644 index 00000000000..7ce887b05c2 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548" + }, + "step": { + "user": { + "data": { + "place_id": "\u5730\u9ede ID", + "service_id": "\u670d\u52d9 ID" + } + } + } + } +} \ No newline at end of file From 3e46c42307b3ea18a5254b58f8ab1a8a805913bb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 13 Nov 2020 22:37:09 -0800 Subject: [PATCH 057/430] Catch the right nest stream refresh exception error (#43189) --- homeassistant/components/nest/camera_sdm.py | 4 ++-- tests/components/nest/camera_sdm_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index ad4293fde5d..a8c5c86c4e8 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,10 +4,10 @@ import datetime import logging from typing import Optional +from aiohttp.client_exceptions import ClientError from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait from google_nest_sdm.device import Device from haffmpeg.tools import IMAGE_JPEG -import requests from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image @@ -130,7 +130,7 @@ class NestCamera(Camera): self._stream_refresh_unsub = None try: self._stream = await self._stream.extend_rtsp_stream() - except requests.HTTPError as err: + except ClientError as err: _LOGGER.debug("Failed to extend stream: %s", err) # Next attempt to catch a url will get a new one self._stream = None diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 40972d04351..e3397129ec9 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -8,9 +8,9 @@ pubsub subscriber. import datetime from typing import List +from aiohttp.client_exceptions import ClientConnectionError from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device import Device -from requests import HTTPError from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE @@ -315,8 +315,8 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client): }, } ), - # Extending the stream fails - FakeResponse(error=HTTPError(response="Some Error")), + # Extending the stream fails with arbitrary error + FakeResponse(error=ClientConnectionError()), # Next attempt to get a stream fetches a new url FakeResponse( { From bf955f3eb8434d2e1f3387e4391b922a6f41e7da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Nov 2020 00:12:00 -1000 Subject: [PATCH 058/430] Fix typo in lw12wifi shoud_poll (#43213) --- homeassistant/components/lw12wifi/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 907e6b898d6..4b8fb2e9ee0 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -113,7 +113,7 @@ class LW12WiFi(LightEntity): return True @property - def shoud_poll(self) -> bool: + def should_poll(self) -> bool: """Return False to not poll the state of this entity.""" return False From 293f8eaaf3cb7b961a09d963a30a60a5b15ce9a6 Mon Sep 17 00:00:00 2001 From: Thomas Delaet Date: Sat, 14 Nov 2020 11:53:59 +0100 Subject: [PATCH 059/430] Add quarter-hour period feature for utility_meter component (#41999) * add support for quarter-hourly intervals to utility meter (15 minutes) * add tests * add test for every quarter * Update homeassistant/components/utility_meter/sensor.py Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .../components/utility_meter/const.py | 12 +++++++- .../components/utility_meter/sensor.py | 12 +++++++- tests/components/utility_meter/test_sensor.py | 28 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 7b55ec4dcd0..39fd952327b 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -1,6 +1,7 @@ """Constants for the utility meter component.""" DOMAIN = "utility_meter" +QUARTER_HOURLY = "quarter-hourly" HOURLY = "hourly" DAILY = "daily" WEEKLY = "weekly" @@ -9,7 +10,16 @@ BIMONTHLY = "bimonthly" QUARTERLY = "quarterly" YEARLY = "yearly" -METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY] +METER_TYPES = [ + QUARTER_HOURLY, + HOURLY, + DAILY, + WEEKLY, + MONTHLY, + BIMONTHLY, + QUARTERLY, + YEARLY, +] DATA_UTILITY = "utility_meter_data" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index e365e77071c..9a4ed9e7782 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -36,6 +36,7 @@ from .const import ( DATA_UTILITY, HOURLY, MONTHLY, + QUARTER_HOURLY, QUARTERLY, SERVICE_CALIBRATE_METER, SIGNAL_RESET_METER, @@ -241,7 +242,16 @@ class UtilityMeterSensor(RestoreEntity): """Handle entity which will be added.""" await super().async_added_to_hass() - if self._period == HOURLY: + if self._period == QUARTER_HOURLY: + for quarter in range(4): + async_track_time_change( + self.hass, + self._async_reset_meter, + minute=(quarter * 15) + + self._period_offset.seconds % (15 * 60) // 60, + second=self._period_offset.seconds % 60, + ) + elif self._period == HOURLY: 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 6a2de3a1cd0..2856a63b4f5 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -264,6 +264,34 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): assert state.state == "5" +async def test_self_reset_quarter_hourly(hass, legacy_patchable_time): + """Test quarter-hourly reset of meter.""" + await _test_self_reset( + hass, gen_config("quarter-hourly"), "2017-12-31T23:59:00.000000+00:00" + ) + + +async def test_self_reset_quarter_hourly_first_quarter(hass, legacy_patchable_time): + """Test quarter-hourly reset of meter.""" + await _test_self_reset( + hass, gen_config("quarter-hourly"), "2017-12-31T23:14:00.000000+00:00" + ) + + +async def test_self_reset_quarter_hourly_second_quarter(hass, legacy_patchable_time): + """Test quarter-hourly reset of meter.""" + await _test_self_reset( + hass, gen_config("quarter-hourly"), "2017-12-31T23:29:00.000000+00:00" + ) + + +async def test_self_reset_quarter_hourly_third_quarter(hass, legacy_patchable_time): + """Test quarter-hourly reset of meter.""" + await _test_self_reset( + hass, gen_config("quarter-hourly"), "2017-12-31T23:44:00.000000+00:00" + ) + + async def test_self_reset_hourly(hass, legacy_patchable_time): """Test hourly reset of meter.""" await _test_self_reset( From dc8db033b940f36a21bd48e724b20249e7037e8c Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sat, 14 Nov 2020 05:57:36 -0500 Subject: [PATCH 060/430] Update greeclimate to 0.10.2 (#43206) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 26ad518bd74..2d2d293085b 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,6 +3,6 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.9.6"], + "requirements": ["greeclimate==0.10.2"], "codeowners": ["@cmroche"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index e83f79096a0..1b8b9ff4d4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -705,7 +705,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.9.6 +greeclimate==0.10.2 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 987066ee41b..610e0f4e337 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -364,7 +364,7 @@ google-cloud-pubsub==2.1.0 google-nest-sdm==0.1.14 # homeassistant.components.gree -greeclimate==0.9.6 +greeclimate==0.10.2 # homeassistant.components.griddy griddypower==0.1.0 From cc396b97368c93dd0452ceef74a8c27691370262 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 14 Nov 2020 12:07:22 +0000 Subject: [PATCH 061/430] Add initial camera support to homekit_controller (#43100) --- .../components/homekit_controller/__init__.py | 12 + .../components/homekit_controller/camera.py | 50 + .../homekit_controller/connection.py | 20 +- .../components/homekit_controller/const.py | 1 + .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_anker_eufycam.py | 53 + .../homekit_controller/test_camera.py | 29 + .../homekit_controller/anker_eufycam.json | 2073 +++++++++++++++++ 10 files changed, 2240 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/homekit_controller/camera.py create mode 100644 tests/components/homekit_controller/specific_devices/test_anker_eufycam.py create mode 100644 tests/components/homekit_controller/test_camera.py create mode 100644 tests/fixtures/homekit_controller/anker_eufycam.json diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index b7139741be2..ef0fb531b1b 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -38,6 +38,8 @@ class HomeKitEntity(Entity): self._signals = [] + super().__init__() + @property def accessory(self) -> Accessory: """Return an Accessory model that this entity is attached to.""" @@ -171,6 +173,16 @@ class HomeKitEntity(Entity): raise NotImplementedError +class AccessoryEntity(HomeKitEntity): + """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-aid:{self._aid}" + + async def async_setup_entry(hass, entry): """Set up a HomeKit connection on a config entry.""" conn = HKDevice(hass, entry, entry.data) diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py new file mode 100644 index 00000000000..fc6a5bb4522 --- /dev/null +++ b/homeassistant/components/homekit_controller/camera.py @@ -0,0 +1,50 @@ +"""Support for Homekit cameras.""" +from aiohomekit.model.services import ServicesTypes + +from homeassistant.components.camera import Camera +from homeassistant.core import callback + +from . import KNOWN_DEVICES, AccessoryEntity + + +class HomeKitCamera(AccessoryEntity, Camera): + """Representation of a Homekit camera.""" + + # content_type = "image/jpeg" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [] + + @property + def state(self): + """Return the current state of the camera.""" + return "idle" + + async def async_camera_image(self): + """Return a jpeg with the current camera snapshot.""" + return await self._accessory.pairing.image( + self._aid, + 640, + 480, + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit sensors.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_accessory(accessory): + stream_mgmt = accessory.services.first( + service_type=ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT + ) + if not stream_mgmt: + return + + info = {"aid": accessory.aid, "iid": stream_mgmt.iid} + async_add_entities([HomeKitCamera(conn, info)], True) + return True + + conn.add_accessory_factory(async_add_accessory) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index ed2aaaa4656..9ba6ef98a02 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -76,6 +76,9 @@ class HKDevice: self.entity_map = Accessories() + # A list of callbacks that turn HK accessories into entities + self.accessory_factories = [] + # A list of callbacks that turn HK service metadata into entities self.listeners = [] @@ -289,14 +292,29 @@ class HKDevice: return True + def add_accessory_factory(self, add_entities_cb): + """Add a callback to run when discovering new entities for accessories.""" + self.accessory_factories.append(add_entities_cb) + self._add_new_entities_for_accessory([add_entities_cb]) + + def _add_new_entities_for_accessory(self, handlers): + for accessory in self.entity_map.accessories: + for handler in handlers: + if (accessory.aid, None) in self.entities: + continue + if handler(accessory): + self.entities.append((accessory.aid, None)) + break + def add_listener(self, add_entities_cb): - """Add a callback to run when discovering new entities.""" + """Add a callback to run when discovering new entities for services.""" self.listeners.append(add_entities_cb) self._add_new_entities([add_entities_cb]) def add_entities(self): """Process the entity map and create HA entities.""" self._add_new_entities(self.listeners) + self._add_new_entities_for_accessory(self.accessory_factories) def _add_new_entities(self, callbacks): for accessory in self.accessories: diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index b1e32417137..b3c55ba36a9 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -37,4 +37,5 @@ HOMEKIT_ACCESSORY_DISPATCH = { "occupancy": "binary_sensor", "television": "media_player", "valve": "switch", + "camera-rtp-stream-management": "camera", } diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index efe842bad0f..45c493ad864 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.54" + "aiohomekit==0.2.57" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 1b8b9ff4d4d..60ce66fe458 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.54 +aiohomekit==0.2.57 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 610e0f4e337..fcd53e2be5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.54 +aiohomekit==0.2.57 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py new file mode 100644 index 00000000000..45e0466ceea --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py @@ -0,0 +1,53 @@ +"""Test against characteristics captured from a eufycam.""" + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_eufycam_setup(hass): + """Test that a eufycam can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "anker_eufycam.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the camera is correctly found and set up + camera_id = "camera.eufycam2_0000" + camera = entity_registry.async_get(camera_id) + assert camera.unique_id == "homekit-A0000A000000000D-aid:4" + + camera_helper = Helper( + hass, + "camera.eufycam2_0000", + pairing, + accessories[0], + config_entry, + ) + + camera_state = await camera_helper.poll_and_get_state() + assert camera_state.attributes["friendly_name"] == "eufyCam2-0000" + assert camera_state.state == "idle" + assert camera_state.attributes["supported_features"] == 0 + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(camera.device_id) + assert device.manufacturer == "Anker" + assert device.name == "eufyCam2-0000" + assert device.model == "T8113" + assert device.sw_version == "1.6.7" + + # These cameras are via a bridge, so via should be set + assert device.via_device_id is not None + + cameras_count = 0 + for state in hass.states.async_all(): + if state.entity_id.startswith("camera."): + cameras_count += 1 + + # There are multiple rtsp services, we only want to create 1 + # camera entity per accessory, not 1 camera per service. + assert cameras_count == 3 diff --git a/tests/components/homekit_controller/test_camera.py b/tests/components/homekit_controller/test_camera.py new file mode 100644 index 00000000000..ddb25fddffc --- /dev/null +++ b/tests/components/homekit_controller/test_camera.py @@ -0,0 +1,29 @@ +"""Basic checks for HomeKit cameras.""" +import base64 + +from aiohomekit.model.services import ServicesTypes +from aiohomekit.testing import FAKE_CAMERA_IMAGE + +from homeassistant.components import camera + +from tests.components.homekit_controller.common import setup_test_component + + +def create_camera(accessory): + """Define camera characteristics.""" + accessory.add_service(ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT) + + +async def test_read_state(hass, utcnow): + """Test reading the state of a HomeKit camera.""" + helper = await setup_test_component(hass, create_camera) + + state = await helper.poll_and_get_state() + assert state.state == "idle" + + +async def test_get_image(hass, utcnow): + """Test getting a JPEG from a camera.""" + helper = await setup_test_component(hass, create_camera) + image = await camera.async_get_image(hass, helper.entity_id) + assert image.content == base64.b64decode(FAKE_CAMERA_IMAGE) diff --git a/tests/fixtures/homekit_controller/anker_eufycam.json b/tests/fixtures/homekit_controller/anker_eufycam.json new file mode 100644 index 00000000000..b3ebfcf7c9f --- /dev/null +++ b/tests/fixtures/homekit_controller/anker_eufycam.json @@ -0,0 +1,2073 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8010", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufy HomeBase2-0AAA", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000A", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.1.6", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "3.0;17A93g", + "perms": [ + "pr", + "hd" + ], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 19, + "type": "80CF79D6-9D29-4268-83F7-58FA0244B7CE", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 20, + "type": "B6704D2D-682B-4CB5-9150-AF94EFD18C22", + "format": "string", + "perms": [ + "pw" + ], + "maxLen": 256 + }, + { + "iid": 21, + "type": "489F0737-E399-41C1-A38A-BC2C152DC88D", + "format": "string", + "value": "0|0", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 22, + "type": "DAC539C4-2E71-4C5F-97BE-47A11B41DE4A", + "format": "string", + "value": "0", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 23, + "type": "7BD15050-677E-446B-983F-CA276A96ECDF", + "format": "string", + "value": "0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 24, + "type": "DBE912DF-223D-4038-8116-D0DFA1B6E3DF", + "format": "string", + "value": "T8010N2319490CEB", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "Unknown Service: 80CF79D6-9D29-4268-83F7-58FA0244B7CE" + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "service" + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8113", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufyCam2-000A", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000B", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.6.7", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 48, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 51, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 52, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 53, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 54, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + }, + { + "iid": 56, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 64, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 66, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 67, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 68, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 69, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 70, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 71, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + }, + { + "iid": 72, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 128, + "type": "204", + "primary": false, + "hidden": false, + "linked": [ + 112, + 160 + ], + "characteristics": [ + { + "iid": 130, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 131, + "type": "205", + "format": "tlv8", + "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 132, + "type": "206", + "format": "tlv8", + "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 133, + "type": "207", + "format": "tlv8", + "value": "AQ4BAQECCQEBAQIBAAMBAQ==", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 134, + "type": "209", + "format": "tlv8", + "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + }, + { + "iid": 135, + "type": "226", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "Unknown Service: 204" + }, + { + "iid": 112, + "type": "00000129-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 114, + "type": "00000130-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQMBAQA=", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 115, + "type": "00000131-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw", + "wr" + ], + "ev": false + }, + { + "iid": 116, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "data-stream-transport-management" + }, + { + "iid": 144, + "type": "21A", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 146, + "type": "223", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 147, + "type": "225", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 148, + "type": "21B", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 149, + "type": "21C", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 150, + "type": "21D", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 152, + "type": "0000011B-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + } + ], + "stype": "Unknown Service: 21A" + }, + { + "iid": 80, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Microphone", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 83, + "type": "0000011A-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + }, + { + "iid": 84, + "type": "00000119-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + } + ], + "stype": "microphone" + }, + { + "iid": 160, + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 162, + "type": "00000022-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true + }, + { + "iid": 163, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Motion Sensor", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 164, + "type": "00000075-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ], + "stype": "motion" + }, + { + "iid": 101, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 102, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 38, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 103, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 104, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "battery" + } + ] + }, + { + "aid": 3, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8113", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufyCam2-000A", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000C", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.6.7", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 48, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 51, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 52, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 53, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 54, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + }, + { + "iid": 56, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 64, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 66, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 67, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 68, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 69, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 70, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 71, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + }, + { + "iid": 72, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 128, + "type": "204", + "primary": false, + "hidden": false, + "linked": [ + 112, + 160 + ], + "characteristics": [ + { + "iid": 130, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 131, + "type": "205", + "format": "tlv8", + "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 132, + "type": "206", + "format": "tlv8", + "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 133, + "type": "207", + "format": "tlv8", + "value": "AQ4BAQECCQEBAQIBAAMBAQ==", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 134, + "type": "209", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + }, + { + "iid": 135, + "type": "226", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "Unknown Service: 204" + }, + { + "iid": 112, + "type": "00000129-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 114, + "type": "00000130-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQMBAQA=", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 115, + "type": "00000131-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw", + "wr" + ], + "ev": false + }, + { + "iid": 116, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "data-stream-transport-management" + }, + { + "iid": 144, + "type": "21A", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 146, + "type": "223", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 147, + "type": "225", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 148, + "type": "21B", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 149, + "type": "21C", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 150, + "type": "21D", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 152, + "type": "0000011B-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + } + ], + "stype": "Unknown Service: 21A" + }, + { + "iid": 80, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Microphone", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 83, + "type": "0000011A-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + }, + { + "iid": 84, + "type": "00000119-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + } + ], + "stype": "microphone" + }, + { + "iid": 160, + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 162, + "type": "00000022-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": [ + "pr", + "ev" + ], + "ev": true + }, + { + "iid": 163, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Motion Sensor", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 164, + "type": "00000075-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ], + "stype": "motion" + }, + { + "iid": 101, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 102, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 103, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 104, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "battery" + } + ] + }, + { + "aid": 4, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8113", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufyCam2-0000", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000D", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.6.7", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 48, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 51, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 52, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 53, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 54, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + }, + { + "iid": 56, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 64, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 66, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 67, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 68, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 69, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 70, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 71, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + }, + { + "iid": 72, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw" + ], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 128, + "type": "204", + "primary": false, + "hidden": false, + "linked": [ + 112, + 160 + ], + "characteristics": [ + { + "iid": 130, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 131, + "type": "205", + "format": "tlv8", + "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 132, + "type": "206", + "format": "tlv8", + "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 133, + "type": "207", + "format": "tlv8", + "value": "AQ4BAQECCQEBAQIBAAMBAQ==", + "perms": [ + "pr", + "ev" + ], + "ev": false + }, + { + "iid": 134, + "type": "209", + "format": "tlv8", + "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + }, + { + "iid": 135, + "type": "226", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev", + "tw" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "Unknown Service: 204" + }, + { + "iid": 112, + "type": "00000129-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 114, + "type": "00000130-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQMBAQA=", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 115, + "type": "00000131-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw", + "wr" + ], + "ev": false + }, + { + "iid": 116, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": [ + "pr" + ], + "ev": false + } + ], + "stype": "data-stream-transport-management" + }, + { + "iid": 144, + "type": "21A", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 146, + "type": "223", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 147, + "type": "225", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 148, + "type": "21B", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 149, + "type": "21C", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 150, + "type": "21D", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 152, + "type": "0000011B-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + } + ], + "stype": "Unknown Service: 21A" + }, + { + "iid": 80, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Microphone", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 83, + "type": "0000011A-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false + }, + { + "iid": 84, + "type": "00000119-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 50, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + } + ], + "stype": "microphone" + }, + { + "iid": 160, + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 162, + "type": "00000022-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true + }, + { + "iid": 163, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Motion Sensor", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 164, + "type": "00000075-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ], + "stype": "motion" + }, + { + "iid": 101, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 102, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 17, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 103, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 104, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "battery" + } + ] + } +] \ No newline at end of file From bb31de1de7320da94268a2277933ff9c323aceee Mon Sep 17 00:00:00 2001 From: b4dpxl <26406101+b4dpxl@users.noreply.github.com> Date: Sat, 14 Nov 2020 13:59:41 +0000 Subject: [PATCH 062/430] Add support for Broadlink BG1 devices (#42314) * Support for BG1 switches after config flow updates to core Broadlink component * updates based on @felipediel feedback * Update updater.py * Update switch.py * Update switch.py --- homeassistant/components/broadlink/const.py | 2 +- homeassistant/components/broadlink/switch.py | 47 +++++++++++++++++++ homeassistant/components/broadlink/updater.py | 9 ++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index adb5a437ae5..b10f7e74ba7 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -8,7 +8,7 @@ DOMAIN = "broadlink" DOMAINS_AND_TYPES = ( (REMOTE_DOMAIN, ("RM2", "RM4")), (SENSOR_DOMAIN, ("A1", "RM2", "RM4")), - (SWITCH_DOMAIN, ("MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")), + (SWITCH_DOMAIN, ("BG1", "MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")), ) DEFAULT_PORT = 80 diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 644255d7d17..b4cd43ac493 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -1,5 +1,6 @@ """Support for Broadlink switches.""" from abc import ABC, abstractmethod +from functools import partial import logging from broadlink.exceptions import BroadlinkException @@ -124,6 +125,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif device.api.type in {"SP4", "SP4B"}: switches = [BroadlinkSP4Switch(device)] + elif device.api.type == "BG1": + switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)] + elif device.api.type == "MP1": switches = [BroadlinkMP1Slot(device, slot) for slot in range(1, 5)] @@ -360,3 +364,46 @@ class BroadlinkMP1Slot(BroadlinkSwitch): _LOGGER.error("Failed to send packet: %s", err) return False return True + + +class BroadlinkBG1Slot(BroadlinkSwitch): + """Representation of a Broadlink BG1 slot.""" + + def __init__(self, device, slot): + """Initialize the switch.""" + super().__init__(device, 1, 0) + self._slot = slot + self._state = self._coordinator.data[f"pwr{slot}"] + self._device_class = DEVICE_CLASS_OUTLET + + @property + def unique_id(self): + """Return the unique id of the slot.""" + return f"{self._device.unique_id}-s{self._slot}" + + @property + def name(self): + """Return the name of the switch.""" + return f"{self._device.name} S{self._slot}" + + @property + def assumed_state(self): + """Return True if unable to access real state of the switch.""" + return False + + @callback + def update_data(self): + """Update data.""" + if self._coordinator.last_update_success: + self._state = self._coordinator.data[f"pwr{self._slot}"] + self.async_write_ha_state() + + async def _async_send_packet(self, packet): + """Send a packet to the device.""" + set_state = partial(self._device.api.set_state, **{f"pwr{self._slot}": packet}) + try: + await self._device.async_request(set_state) + except (BroadlinkException, OSError) as err: + _LOGGER.error("Failed to send packet: %s", err) + return False + return True diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index eb42e688a59..c9b273218b5 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -26,6 +26,7 @@ def get_update_manager(device): update_managers = { "A1": BroadlinkA1UpdateManager, + "BG1": BroadlinkBG1UpdateManager, "MP1": BroadlinkMP1UpdateManager, "RM2": BroadlinkRMUpdateManager, "RM4": BroadlinkRMUpdateManager, @@ -161,6 +162,14 @@ class BroadlinkSP2UpdateManager(BroadlinkUpdateManager): return data +class BroadlinkBG1UpdateManager(BroadlinkUpdateManager): + """Manages updates for Broadlink BG1 devices.""" + + async def async_fetch_data(self): + """Fetch data from the device.""" + return await self.device.async_request(self.device.api.get_state) + + class BroadlinkSP4UpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink SP4 devices.""" From c6608f7f499de7ffba69766edf77f7d873447704 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sat, 14 Nov 2020 12:13:37 -0500 Subject: [PATCH 063/430] Bump elkm1-lib to 0.8.8 (#43230) --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 1875ce4e970..769e5c37dd7 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==0.8.7"], + "requirements": ["elkm1-lib==0.8.8"], "codeowners": ["@gwww", "@bdraco"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 60ce66fe458..b5050e4d080 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -544,7 +544,7 @@ elgato==0.2.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.8.7 +elkm1-lib==0.8.8 # homeassistant.components.mobile_app emoji==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcd53e2be5e..168af96ee07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -287,7 +287,7 @@ eebrightbox==0.0.4 elgato==0.2.0 # homeassistant.components.elkm1 -elkm1-lib==0.8.7 +elkm1-lib==0.8.8 # homeassistant.components.mobile_app emoji==0.5.4 From d1566bd21025527e8575eb5f342445b569224dc1 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Nov 2020 12:59:13 -0500 Subject: [PATCH 064/430] Add HomeKit humidifier/dehumidifier (#42311) * add HomeKit humidifier/dehumidifier * added more test coverage * simplified char logic Co-authored-by: Quentame * use mode constants * Renamed HomeKit Contorller Co-authored-by: Quentame * improved threshold logic * split up homekit humidifier into 2 entities * fixed tests * fixed mode and switch logic * added set mode tests * removed redundant methods present in base class Co-authored-by: Quentame --- .../components/homekit_controller/const.py | 1 + .../homekit_controller/humidifier.py | 304 ++++++++++++++++ .../homekit_controller/test_humidifier.py | 333 ++++++++++++++++++ 3 files changed, 638 insertions(+) create mode 100644 homeassistant/components/homekit_controller/humidifier.py create mode 100644 tests/components/homekit_controller/test_humidifier.py diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index b3c55ba36a9..c3af1033148 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -25,6 +25,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "motion": "binary_sensor", "carbon-dioxide": "sensor", "humidity": "sensor", + "humidifier-dehumidifier": "humidifier", "light": "sensor", "temperature": "sensor", "battery": "sensor", diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py new file mode 100644 index 00000000000..10ffee198e4 --- /dev/null +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -0,0 +1,304 @@ +"""Support for HomeKit Controller humidifier.""" +from typing import List, Optional + +from aiohomekit.model.characteristics import CharacteristicsTypes + +from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier.const import ( + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + MODE_AUTO, + MODE_NORMAL, + SUPPORT_MODES, +) +from homeassistant.core import callback + +from . import KNOWN_DEVICES, HomeKitEntity + +SUPPORT_FLAGS = 0 + +HK_MODE_TO_HA = { + 0: "off", + 1: MODE_AUTO, + 2: "humidifying", + 3: "dehumidifying", +} + +HA_MODE_TO_HK = { + MODE_AUTO: 0, + "humidifying": 1, + "dehumidifying": 2, +} + + +class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): + """Representation of a HomeKit Controller Humidifier.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.ACTIVE, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, + ] + + @property + def device_class(self) -> str: + """Return the device class of the device.""" + return DEVICE_CLASS_HUMIDIFIER + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS | SUPPORT_MODES + + @property + def is_on(self): + """Return true if device is on.""" + return self.service.value(CharacteristicsTypes.ACTIVE) + + async def async_turn_on(self, **kwargs): + """Turn the specified valve on.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) + + async def async_turn_off(self, **kwargs): + """Turn the specified valve off.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + + @property + def target_humidity(self) -> Optional[int]: + """Return the humidity we try to reach.""" + return self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + + @property + def mode(self) -> Optional[str]: + """Return the current mode, e.g., home, auto, baby. + + Requires SUPPORT_MODES. + """ + mode = self.service.value( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + return MODE_AUTO if mode == 1 else MODE_NORMAL + + @property + def available_modes(self) -> Optional[List[str]]: + """Return a list of available modes. + + Requires SUPPORT_MODES. + """ + available_modes = [ + MODE_NORMAL, + MODE_AUTO, + ] + + return available_modes + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self.async_put_characteristics( + {CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: humidity} + ) + + async def async_set_mode(self, mode: str) -> None: + """Set new mode.""" + if mode == MODE_AUTO: + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + CharacteristicsTypes.ACTIVE: True, + } + ) + else: + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + CharacteristicsTypes.ACTIVE: True, + } + ) + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ].minValue + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ].maxValue + + +class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): + """Representation of a HomeKit Controller Humidifier.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.ACTIVE, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD, + ] + + @property + def device_class(self) -> str: + """Return the device class of the device.""" + return DEVICE_CLASS_DEHUMIDIFIER + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS | SUPPORT_MODES + + @property + def is_on(self): + """Return true if device is on.""" + return self.service.value(CharacteristicsTypes.ACTIVE) + + async def async_turn_on(self, **kwargs): + """Turn the specified valve on.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) + + async def async_turn_off(self, **kwargs): + """Turn the specified valve off.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + + @property + def target_humidity(self) -> Optional[int]: + """Return the humidity we try to reach.""" + return self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + + @property + def mode(self) -> Optional[str]: + """Return the current mode, e.g., home, auto, baby. + + Requires SUPPORT_MODES. + """ + mode = self.service.value( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + return MODE_AUTO if mode == 1 else MODE_NORMAL + + @property + def available_modes(self) -> Optional[List[str]]: + """Return a list of available modes. + + Requires SUPPORT_MODES. + """ + available_modes = [ + MODE_NORMAL, + MODE_AUTO, + ] + + return available_modes + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self.async_put_characteristics( + {CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: humidity} + ) + + async def async_set_mode(self, mode: str) -> None: + """Set new mode.""" + if mode == MODE_AUTO: + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + CharacteristicsTypes.ACTIVE: True, + } + ) + else: + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + CharacteristicsTypes.ACTIVE: True, + } + ) + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ].minValue + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ].maxValue + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-{self._iid}-{self.device_class}" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit humidifer.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def get_accessory(conn, aid): + for acc in conn.accessories: + if acc.get("aid") == aid: + return acc + return None + + def get_service(acc, iid): + for serv in acc.get("services"): + if serv.get("iid") == iid: + return serv + return None + + def get_char(serv, iid): + try: + type_name = CharacteristicsTypes[iid] + type_uuid = CharacteristicsTypes.get_uuid(type_name) + for char in serv.get("characteristics"): + if char.get("type") == type_uuid: + return char + except KeyError: + return None + return None + + @callback + def async_add_service(aid, service): + if service["stype"] != "humidifier-dehumidifier": + return False + info = {"aid": aid, "iid": service["iid"]} + + acc = get_accessory(conn, aid) + serv = get_service(acc, service["iid"]) + + if ( + get_char(serv, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD) + is not None + ): + async_add_entities([HomeKitHumidifier(conn, info)], True) + + if ( + get_char( + serv, CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + is not None + ): + async_add_entities([HomeKitDehumidifier(conn, info)], True) + + return True + + conn.add_listener(async_add_service) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py new file mode 100644 index 00000000000..0af795e2ce9 --- /dev/null +++ b/tests/components/homekit_controller/test_humidifier.py @@ -0,0 +1,333 @@ +"""Basic checks for HomeKit Humidifier/Dehumidifier.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from homeassistant.components.humidifier import DOMAIN +from homeassistant.components.humidifier.const import MODE_AUTO, MODE_NORMAL + +from tests.components.homekit_controller.common import setup_test_component + +ACTIVE = ("humidifier-dehumidifier", "active") +CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE = ( + "humidifier-dehumidifier", + "humidifier-dehumidifier.state.current", +) +TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE = ( + "humidifier-dehumidifier", + "humidifier-dehumidifier.state.target", +) +RELATIVE_HUMIDITY_CURRENT = ("humidifier-dehumidifier", "relative-humidity.current") +RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD = ( + "humidifier-dehumidifier", + "relative-humidity.humidifier-threshold", +) +RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD = ( + "humidifier-dehumidifier", + "relative-humidity.dehumidifier-threshold", +) + + +def create_humidifier_service(accessory): + """Define a humidifier characteristics as per page 219 of HAP spec.""" + service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) + + service.add_char(CharacteristicsTypes.ACTIVE, value=False) + + cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) + cur_state.value = 0 + + cur_state = service.add_char( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + cur_state.value = -1 + + targ_state = service.add_char( + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + targ_state.value = 0 + + cur_state = service.add_char( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + cur_state.value = 0 + + return service + + +def create_dehumidifier_service(accessory): + """Define a dehumidifier characteristics as per page 219 of HAP spec.""" + service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) + + service.add_char(CharacteristicsTypes.ACTIVE, value=False) + + cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) + cur_state.value = 0 + + cur_state = service.add_char( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + cur_state.value = -1 + + targ_state = service.add_char( + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + targ_state.value = 0 + + targ_state = service.add_char( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + targ_state.value = 0 + + return service + + +async def test_humidifier_active_state(hass, utcnow): + """Test that we can turn a HomeKit humidifier on and off again.""" + helper = await setup_test_component(hass, create_humidifier_service) + + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 0 + + +async def test_dehumidifier_active_state(hass, utcnow): + """Test that we can turn a HomeKit dehumidifier on and off again.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 0 + + +async def test_humidifier_read_humidity(hass, utcnow): + """Test that we can read the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) + + helper.characteristics[ACTIVE].value = True + helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 75 + state = await helper.poll_and_get_state() + assert state.state == "on" + assert state.attributes["humidity"] == 75 + + helper.characteristics[ACTIVE].value = False + helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 10 + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["humidity"] == 10 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["humidity"] == 10 + + +async def test_dehumidifier_read_humidity(hass, utcnow): + """Test that we can read the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + helper.characteristics[ACTIVE].value = True + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 75 + state = await helper.poll_and_get_state() + assert state.state == "on" + assert state.attributes["humidity"] == 75 + + helper.characteristics[ACTIVE].value = False + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40 + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["humidity"] == 40 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["humidity"] == 40 + + +async def test_humidifier_set_humidity(hass, utcnow): + """Test that we can set the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_humidity", + {"entity_id": helper.entity_id, "humidity": 20}, + blocking=True, + ) + assert helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value == 20 + + +async def test_dehumidifier_set_humidity(hass, utcnow): + """Test that we can set the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_humidity", + {"entity_id": helper.entity_id, "humidity": 20}, + blocking=True, + ) + assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 20 + + +async def test_humidifier_set_mode(hass, utcnow): + """Test that we can set the mode of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_AUTO}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0 + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_NORMAL}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 1 + assert helper.characteristics[ACTIVE].value == 1 + + +async def test_dehumidifier_set_mode(hass, utcnow): + """Test that we can set the mode of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_AUTO}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0 + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_NORMAL}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 2 + assert helper.characteristics[ACTIVE].value == 1 + + +async def test_humidifier_read_only_mode(hass, utcnow): + """Test that we can read the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + +async def test_dehumidifier_read_only_mode(hass, utcnow): + """Test that we can read the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + +async def test_humidifier_target_humidity_modes(hass, utcnow): + """Test that we can read the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) + + helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 37 + helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + assert state.attributes["humidity"] == 37 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 37 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 37 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 37 + + +async def test_dehumidifier_target_humidity_modes(hass, utcnow): + """Test that we can read the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 73 + helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 73 From a3e8968e8a796bca5681fe5ce6cc1b1031952e30 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sat, 14 Nov 2020 15:16:14 -0500 Subject: [PATCH 065/430] Remove OpenCV dependecy from Environment Canada (#43235) * Bump env_canada to 0.2.2 * Revert PR #38731 --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index f89a7eb3e55..c605e92acd5 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.0"], + "requirements": ["env_canada==0.2.2"], "codeowners": ["@michaeldavie"] } diff --git a/requirements_all.txt b/requirements_all.txt index b5050e4d080..a44f607f8c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -# env_canada==0.2.0 +env_canada==0.2.2 # homeassistant.components.envirophat # envirophat==0.0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c3e489c1ebb..f627346c67b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -24,7 +24,6 @@ COMMENT_REQUIREMENTS = ( "credstash", "decora", "decora_wifi", - "env_canada", "envirophat", "evdev", "face_recognition", From 085aa3c99d6b27a2ddfbdb12ecce5bc077f1d406 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 14 Nov 2020 21:41:04 +0100 Subject: [PATCH 066/430] Upgrade youtube_dl to version 2020.11.12 (#43231) --- 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 7d6e92ed274..b834cbc0aab 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.11.01.1"], + "requirements": ["youtube_dl==2020.11.12"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index a44f607f8c4..efef5f2c5d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2326,7 +2326,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.11.01.1 +youtube_dl==2020.11.12 # homeassistant.components.onvif zeep[async]==4.0.0 From 810561e313e6de57d5fbe443247126d4704a8914 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Nov 2020 10:43:49 -1000 Subject: [PATCH 067/430] Switch ios to dispatching instead of polling (#43233) --- homeassistant/components/ios/__init__.py | 7 +++++-- homeassistant/components/ios/sensor.py | 23 +++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index bc5c5ec1c4e..2feafba949d 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -9,6 +9,7 @@ from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.json import load_json, save_json from .const import ( @@ -346,9 +347,11 @@ class iOSIdentifyDeviceView(HomeAssistantView): data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() - name = data.get(ATTR_DEVICE_ID) + device_id = data[ATTR_DEVICE_ID] - hass.data[DOMAIN][ATTR_DEVICES][name] = data + hass.data[DOMAIN][ATTR_DEVICES][device_id] = data + + async_dispatcher_send(hass, f"{DOMAIN}.{device_id}", data) try: save_json(self._config_path, hass.data[DOMAIN]) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index 01520063ccb..ccbc118a681 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,9 +1,13 @@ """Support for Home Assistant iOS app sensors.""" from homeassistant.components import ios from homeassistant.const import PERCENTAGE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level +from .const import DOMAIN + SENSOR_TYPES = { "level": ["Battery Level", PERCENTAGE], "state": ["Battery State", None], @@ -73,6 +77,11 @@ class IOSSensor(Entity): device_id = self._device[ios.ATTR_DEVICE_ID] return f"{self.type}_{device_id}" + @property + def should_poll(self): + """No polling needed.""" + return False + @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" @@ -114,7 +123,17 @@ class IOSSensor(Entity): return icon_state return icon_for_battery_level(battery_level=battery_level, charging=charging) - async def async_update(self): + @callback + def _update(self, device): """Get the latest state of the sensor.""" - self._device = ios.devices(self.hass).get(self._device_name) + self._device = device self._state = self._device[ios.ATTR_BATTERY][self.type] + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Added to hass so need to register to dispatch.""" + self._state = self._device[ios.ATTR_BATTERY][self.type] + device_id = self._device[ios.ATTR_DEVICE_ID] + self.async_on_remove( + async_dispatcher_connect(self.hass, f"{DOMAIN}.{device_id}", self._update) + ) From 11a437bac948762866b927a9f5b2a3679ebc2bce Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 14 Nov 2020 14:46:01 -0600 Subject: [PATCH 068/430] Bump pyheos to 0.7.2 (#43239) --- homeassistant/components/heos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index a6da3623da7..6505a564560 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -3,7 +3,7 @@ "name": "Denon HEOS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/heos", - "requirements": ["pyheos==0.6.0"], + "requirements": ["pyheos==0.7.2"], "ssdp": [ { "st": "urn:schemas-denon-com:device:ACT-Denon:1" diff --git a/requirements_all.txt b/requirements_all.txt index efef5f2c5d0..94c773ca753 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1422,7 +1422,7 @@ pygti==0.9.2 pyhaversion==3.4.2 # homeassistant.components.heos -pyheos==0.6.0 +pyheos==0.7.2 # homeassistant.components.hikvision pyhik==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 168af96ee07..cd3605147ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -710,7 +710,7 @@ pygti==0.9.2 pyhaversion==3.4.2 # homeassistant.components.heos -pyheos==0.6.0 +pyheos==0.7.2 # homeassistant.components.homematic pyhomematic==0.1.70 From 8b63e22c993ab365fbb966a5d871ac78f2ef24a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Nov 2020 10:46:24 -1000 Subject: [PATCH 069/430] Bypass the slow update warning for group updates (#43209) --- homeassistant/components/group/__init__.py | 11 +++++-- homeassistant/components/group/cover.py | 8 +++++ tests/components/group/test_cover.py | 38 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 4a0050868d9..32a9bd41014 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,4 +1,5 @@ """Provide the functionality to group entities.""" +from abc import abstractmethod import asyncio from contextvars import ContextVar import logging @@ -398,7 +399,8 @@ class GroupEntity(Entity): assert self.hass is not None async def _update_at_start(_): - await self.async_update_ha_state(True) + await self.async_update() + self.async_write_ha_state() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _update_at_start) @@ -409,7 +411,12 @@ class GroupEntity(Entity): if self.hass.state != CoreState.running: return - await self.async_update_ha_state(True) + await self.async_update() + self.async_write_ha_state() + + @abstractmethod + async def async_update(self) -> None: + """Abstract method to update the entity.""" class Group(Entity): diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 25a8665db9f..b52546c48d7 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -150,6 +150,8 @@ class CoverGroup(GroupEntity, CoverEntity): """Register listeners.""" for entity_id in self._entities: new_state = self.hass.states.get(entity_id) + if new_state is None: + continue await self.async_update_supported_features( entity_id, new_state, update_state=False ) @@ -307,6 +309,8 @@ class CoverGroup(GroupEntity, CoverEntity): self._cover_position = 0 if self.is_closed else 100 for entity_id in self._covers[KEY_POSITION]: state = self.hass.states.get(entity_id) + if state is None: + continue pos = state.attributes.get(ATTR_CURRENT_POSITION) if position == -1: position = pos @@ -323,6 +327,8 @@ class CoverGroup(GroupEntity, CoverEntity): self._tilt_position = 100 for entity_id in self._tilts[KEY_POSITION]: state = self.hass.states.get(entity_id) + if state is None: + continue pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION) if position == -1: position = pos @@ -351,6 +357,8 @@ class CoverGroup(GroupEntity, CoverEntity): if not self._assumed_state: for entity_id in self._entities: state = self.hass.states.get(entity_id) + if state is None: + continue if state and state.attributes.get(ATTR_ASSUMED_STATE): self._assumed_state = True break diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 2ffe02570c9..59bde36b46b 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -63,6 +63,16 @@ CONFIG_POS = { ] } +CONFIG_TILT_ONLY = { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + CONF_ENTITIES: [DEMO_COVER_TILT, DEMO_TILT], + }, + ] +} + CONFIG_ATTRIBUTES = { DOMAIN: { "platform": "group", @@ -211,6 +221,34 @@ async def test_attributes(hass, setup_comp): assert state.attributes[ATTR_ASSUMED_STATE] is True +@pytest.mark.parametrize("config_count", [(CONFIG_TILT_ONLY, 2)]) +async def test_cover_that_only_supports_tilt_removed(hass, setup_comp): + """Test removing a cover that support tilt.""" + hass.states.async_set( + DEMO_COVER_TILT, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}, + ) + hass.states.async_set( + DEMO_TILT, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}, + ) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert state.attributes[ATTR_ENTITY_ID] == [ + DEMO_COVER_TILT, + DEMO_TILT, + ] + assert ATTR_ASSUMED_STATE not in state.attributes + assert ATTR_CURRENT_TILT_POSITION in state.attributes + + hass.states.async_remove(DEMO_COVER_TILT) + hass.states.async_set(DEMO_TILT, STATE_CLOSED) + await hass.async_block_till_done() + + @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_open_covers(hass, setup_comp): """Test open cover function.""" From 4e00a8a3d0210c594198522eedc17a866d0080c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Nov 2020 10:47:38 -1000 Subject: [PATCH 070/430] Eliminate doorbird switch polling (#43215) --- homeassistant/components/doorbird/switch.py | 36 +++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index 358f4172346..f1f146aebb9 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -2,6 +2,8 @@ import datetime from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO @@ -37,13 +39,13 @@ class DoorBirdSwitch(DoorBirdEntity, SwitchEntity): self._doorstation = doorstation self._relay = relay self._state = False - self._assume_off = datetime.datetime.min if relay == IR_RELAY: self._time = datetime.timedelta(minutes=5) else: self._time = datetime.timedelta(seconds=5) self._unique_id = f"{self._mac_addr}_{self._relay}" + self._reset_sub = None @property def unique_id(self): @@ -63,27 +65,41 @@ class DoorBirdSwitch(DoorBirdEntity, SwitchEntity): """Return the icon to display.""" return "mdi:lightbulb" if self._relay == IR_RELAY else "mdi:dip-switch" + @property + def should_poll(self): + """No need to poll.""" + return False + @property def is_on(self): """Get the assumed state of the relay.""" return self._state - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): + """Power the relay.""" + if self._reset_sub is not None: + self._reset_sub() + self._reset_sub = None + self._reset_sub = async_track_point_in_utc_time( + self.hass, self._async_turn_off, dt_util.utcnow() + self._time + ) + await self.hass.async_add_executor_job(self._turn_on) + self.async_write_ha_state() + + def _turn_on(self): """Power the relay.""" if self._relay == IR_RELAY: self._state = self._doorstation.device.turn_light_on() else: self._state = self._doorstation.device.energize_relay(self._relay) - now = dt_util.utcnow() - self._assume_off = now + self._time - - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off the relays is not needed. They are time-based.""" raise NotImplementedError("DoorBird relays cannot be manually turned off.") - async def async_update(self): + @callback + def _async_turn_off(self, *_): """Wait for the correct amount of assumed time to pass.""" - if self._state and self._assume_off <= dt_util.utcnow(): - self._state = False - self._assume_off = datetime.datetime.min + self._state = False + self._reset_sub = None + self.async_write_ha_state() From cd42d82f9d5119bf4ea6dcb72942fd2289fa92cc Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 14 Nov 2020 14:57:42 -0600 Subject: [PATCH 071/430] Bump pysmartthings and pysmartapp (#43240) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 30ef278d1d1..88ed85306db 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.7.4"], + "requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"], "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@andrewsayre"] diff --git a/requirements_all.txt b/requirements_all.txt index 94c773ca753..32603901c8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1680,10 +1680,10 @@ pysma==0.3.5 pysmappee==0.2.13 # homeassistant.components.smartthings -pysmartapp==0.3.2 +pysmartapp==0.3.3 # homeassistant.components.smartthings -pysmartthings==0.7.4 +pysmartthings==0.7.6 # homeassistant.components.smarty pysmarty==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd3605147ae..8a0284af15b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -839,10 +839,10 @@ pysma==0.3.5 pysmappee==0.2.13 # homeassistant.components.smartthings -pysmartapp==0.3.2 +pysmartapp==0.3.3 # homeassistant.components.smartthings -pysmartthings==0.7.4 +pysmartthings==0.7.6 # homeassistant.components.soma pysoma==0.0.10 From 84569549f88ec47c1a6af0001dfee324a484b7c9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 14 Nov 2020 17:24:41 -0500 Subject: [PATCH 072/430] Refactor ZHA light initialization (#43149) * Refactor ZHA light initialization * Don't do redundant attribute reads --- .../components/zha/core/channels/lighting.py | 59 +++++++------------ homeassistant/components/zha/light.py | 44 +++++--------- 2 files changed, 37 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 9d52ff12d37..2828193f8cf 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -3,7 +3,7 @@ from typing import Optional import zigpy.zcl.clusters.lighting as lighting -from .. import registries, typing as zha_typing +from .. import registries from ..const import REPORT_CONFIG_DEFAULT from .base import ClientChannel, ZigbeeChannel @@ -32,15 +32,19 @@ class ColorChannel(ZigbeeChannel): {"attr": "current_y", "config": REPORT_CONFIG_DEFAULT}, {"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT}, ) + MAX_MIREDS: int = 500 + MIN_MIREDS: int = 153 - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType - ) -> None: - """Initialize ColorChannel.""" - super().__init__(cluster, ch_pool) - self._color_capabilities = None - self._min_mireds = 153 - self._max_mireds = 500 + @property + def color_capabilities(self) -> int: + """Return color capabilities of the light.""" + try: + return self.cluster["color_capabilities"] + except KeyError: + pass + if self.cluster.get("color_temperature") is not None: + return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP + return self.CAPABILITIES_COLOR_XY @property def color_loop_active(self) -> Optional[int]: @@ -65,49 +69,30 @@ class ColorChannel(ZigbeeChannel): @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_min", self._min_mireds) + return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) @property def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_max", self._max_mireds) + return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) - def get_color_capabilities(self): - """Return the color capabilities.""" - return self._color_capabilities - - async def async_configure(self): + async def async_configure(self) -> None: """Configure channel.""" await self.fetch_color_capabilities(False) await super().async_configure() - async def async_initialize(self, from_cache): + async def async_initialize(self, from_cache: bool) -> None: """Initialize channel.""" await self.fetch_color_capabilities(True) - attributes = ["color_temperature", "current_x", "current_y"] - await self.get_attributes(attributes, from_cache=from_cache) + await super().async_initialize(from_cache) - async def fetch_color_capabilities(self, from_cache): + async def fetch_color_capabilities(self, from_cache: bool) -> None: """Get the color configuration.""" attributes = [ "color_temp_physical_min", "color_temp_physical_max", "color_capabilities", + "color_temperature", ] - results = await self.get_attributes(attributes, from_cache=from_cache) - capabilities = results.get("color_capabilities") - - if capabilities is None: - # ZCL Version 4 devices don't support the color_capabilities - # attribute. In this version XY support is mandatory, but we - # need to probe to determine if the device supports color - # temperature. - capabilities = self.CAPABILITIES_COLOR_XY - result = await self.get_attribute_value( - "color_temperature", from_cache=from_cache - ) - - if result is not None and result is not self.UNSUPPORTED_ATTRIBUTE: - capabilities |= self.CAPABILITIES_COLOR_TEMP - self._color_capabilities = capabilities - await super().async_initialize(from_cache) + # just populates the cache, if not already done + await self.get_attributes(attributes, from_cache=from_cache) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 22ab0cdcb21..1f310c32aab 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -344,7 +344,7 @@ class Light(BaseLight, ZhaEntity): self._brightness = self._level_channel.current_level if self._color_channel: - color_capabilities = self._color_channel.get_color_capabilities() + color_capabilities = self._color_channel.color_capabilities if color_capabilities & CAPABILITIES_COLOR_TEMP: self._supported_features |= light.SUPPORT_COLOR_TEMP self._color_temp = self._color_channel.color_temperature @@ -439,34 +439,20 @@ class Light(BaseLight, ZhaEntity): if level is not None: self._brightness = level if self._color_channel: - attributes = [] - color_capabilities = self._color_channel.get_color_capabilities() - if ( - color_capabilities is not None - and color_capabilities & CAPABILITIES_COLOR_TEMP - ): - attributes.append("color_temperature") - if ( - color_capabilities is not None - and color_capabilities & CAPABILITIES_COLOR_XY - ): - attributes.append("current_x") - attributes.append("current_y") - if ( - color_capabilities is not None - and color_capabilities & CAPABILITIES_COLOR_LOOP - ): - attributes.append("color_loop_active") + attributes = [ + "color_temperature", + "current_x", + "current_y", + "color_loop_active", + ] results = await self._color_channel.get_attributes( attributes, from_cache=from_cache ) - if ( - "color_temperature" in results - and results["color_temperature"] is not None - ): - self._color_temp = results["color_temperature"] + color_temp = results.get("color_temperature") + if color_temp is not None: + self._color_temp = color_temp color_x = results.get("current_x") color_y = results.get("current_y") @@ -474,13 +460,13 @@ class Light(BaseLight, ZhaEntity): self._hs_color = color_util.color_xy_to_hs( float(color_x / 65535), float(color_y / 65535) ) - if ( - "color_loop_active" in results - and results["color_loop_active"] is not None - ): - color_loop_active = results["color_loop_active"] + + color_loop_active = results.get("color_loop_active") + if color_loop_active is not None: if color_loop_active == 1: self._effect = light.EFFECT_COLORLOOP + else: + self._effect = None async def _refresh(self, time): """Call async_get_state at an interval.""" From 9a2142180780ba0c0d9da1857703f8ecae1b163f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 15 Nov 2020 00:03:48 +0000 Subject: [PATCH 073/430] [ci skip] Translation update --- .../components/abode/translations/hu.json | 3 ++ .../components/adguard/translations/hu.json | 3 ++ .../advantage_air/translations/hu.json | 17 +++++++ .../components/agent_dvr/translations/hu.json | 3 ++ .../components/airvisual/translations/hu.json | 6 +++ .../alarmdecoder/translations/hu.json | 10 ++++ .../ambiclimate/translations/hu.json | 7 +++ .../components/arcam_fmj/translations/hu.json | 7 +++ .../components/atag/translations/hu.json | 3 ++ .../components/aurora/translations/hu.json | 26 ++++++++++ .../components/aurora/translations/it.json | 26 ++++++++++ .../components/axis/translations/hu.json | 12 ++++- .../azure_devops/translations/hu.json | 5 ++ .../binary_sensor/translations/hu.json | 16 +++++++ .../components/braviatv/translations/hu.json | 5 ++ .../components/broadlink/translations/ru.json | 2 +- .../components/brother/translations/hu.json | 1 + .../components/bsblan/translations/hu.json | 3 ++ .../cert_expiry/translations/hu.json | 3 ++ .../components/climate/translations/ru.json | 4 +- .../components/cloud/translations/hu.json | 11 +++++ .../cloudflare/translations/hu.json | 33 +++++++++++++ .../coolmaster/translations/hu.json | 3 ++ .../components/daikin/translations/hu.json | 5 ++ .../components/denonavr/translations/hu.json | 6 ++- .../device_tracker/translations/hu.json | 4 ++ .../components/dexcom/translations/hu.json | 10 ++++ .../dialogflow/translations/hu.json | 3 ++ .../components/dsmr/translations/hu.json | 10 ++++ .../components/dsmr/translations/it.json | 10 ++++ .../components/elgato/translations/hu.json | 6 ++- .../components/epson/translations/hu.json | 16 +++++++ .../forked_daapd/translations/hu.json | 7 +++ .../components/freebox/translations/hu.json | 3 ++ .../components/guardian/translations/hu.json | 7 +++ .../components/hassio/translations/hu.json | 14 ++++++ .../components/hassio/translations/it.json | 16 +++++++ .../components/heos/translations/hu.json | 3 ++ .../home_connect/translations/hu.json | 3 ++ .../homeassistant/translations/hu.json | 21 +++++++++ .../homeassistant/translations/it.json | 3 +- .../components/homekit/translations/hu.json | 17 +++++++ .../components/iaqualink/translations/hu.json | 3 ++ .../components/icloud/translations/hu.json | 5 ++ .../components/ifttt/translations/hu.json | 3 ++ .../components/ipp/translations/hu.json | 8 +++- .../components/kodi/translations/ru.json | 2 +- .../components/life360/translations/hu.json | 10 +++- .../components/locative/translations/hu.json | 3 ++ .../components/lovelace/translations/hu.json | 10 ++++ .../components/lovelace/translations/it.json | 3 +- .../components/mailgun/translations/hu.json | 3 ++ .../meteo_france/translations/hu.json | 3 ++ .../minecraft_server/translations/hu.json | 3 ++ .../components/nest/translations/hu.json | 3 ++ .../components/onewire/translations/hu.json | 21 +++++++++ .../components/onvif/translations/hu.json | 13 +++++ .../ovo_energy/translations/hu.json | 3 +- .../components/ozw/translations/hu.json | 26 ++++++++++ .../panasonic_viera/translations/hu.json | 6 +++ .../components/plaato/translations/hu.json | 7 +++ .../components/profiler/translations/hu.json | 12 +++++ .../components/ps4/translations/hu.json | 4 ++ .../rainmachine/translations/hu.json | 9 ++++ .../recollect_waste/translations/hu.json | 18 +++++++ .../recollect_waste/translations/it.json | 18 +++++++ .../components/rfxtrx/translations/hu.json | 10 ++++ .../ruckus_unleashed/translations/hu.json | 17 +++++++ .../components/samsungtv/translations/hu.json | 1 + .../components/smappee/translations/hu.json | 3 +- .../components/smarthab/translations/hu.json | 9 ++++ .../smartthings/translations/hu.json | 3 +- .../components/somfy/translations/hu.json | 3 ++ .../synology_dsm/translations/hu.json | 3 ++ .../components/tibber/translations/hu.json | 1 + .../components/traccar/translations/hu.json | 7 +++ .../components/tuya/translations/hu.json | 47 ++++++++++++++++++- .../components/twilio/translations/hu.json | 3 ++ .../components/unifi/translations/hu.json | 3 ++ .../components/upcloud/translations/hu.json | 25 ++++++++++ .../water_heater/translations/hu.json | 8 ++++ .../components/wiffi/translations/hu.json | 7 +++ .../components/withings/translations/hu.json | 1 + .../components/wled/translations/hu.json | 6 ++- .../components/xbox/translations/hu.json | 7 +++ .../xiaomi_aqara/translations/hu.json | 37 ++++++++++++++- .../xiaomi_miio/translations/hu.json | 18 +++++-- .../components/yeelight/translations/hu.json | 33 ++++++++++++- .../zoneminder/translations/hu.json | 13 +++++ 89 files changed, 812 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/advantage_air/translations/hu.json create mode 100644 homeassistant/components/alarmdecoder/translations/hu.json create mode 100644 homeassistant/components/ambiclimate/translations/hu.json create mode 100644 homeassistant/components/arcam_fmj/translations/hu.json create mode 100644 homeassistant/components/aurora/translations/hu.json create mode 100644 homeassistant/components/aurora/translations/it.json create mode 100644 homeassistant/components/cloud/translations/hu.json create mode 100644 homeassistant/components/cloudflare/translations/hu.json create mode 100644 homeassistant/components/dexcom/translations/hu.json create mode 100644 homeassistant/components/epson/translations/hu.json create mode 100644 homeassistant/components/forked_daapd/translations/hu.json create mode 100644 homeassistant/components/guardian/translations/hu.json create mode 100644 homeassistant/components/homeassistant/translations/hu.json create mode 100644 homeassistant/components/homekit/translations/hu.json create mode 100644 homeassistant/components/lovelace/translations/hu.json create mode 100644 homeassistant/components/onewire/translations/hu.json create mode 100644 homeassistant/components/ozw/translations/hu.json create mode 100644 homeassistant/components/plaato/translations/hu.json create mode 100644 homeassistant/components/profiler/translations/hu.json create mode 100644 homeassistant/components/recollect_waste/translations/hu.json create mode 100644 homeassistant/components/recollect_waste/translations/it.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/hu.json create mode 100644 homeassistant/components/smarthab/translations/hu.json create mode 100644 homeassistant/components/traccar/translations/hu.json create mode 100644 homeassistant/components/upcloud/translations/hu.json create mode 100644 homeassistant/components/water_heater/translations/hu.json create mode 100644 homeassistant/components/wiffi/translations/hu.json create mode 100644 homeassistant/components/xbox/translations/hu.json create mode 100644 homeassistant/components/zoneminder/translations/hu.json diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index 160810c8211..77ce53abef7 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett." }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 1ca56c7684f..3f67c765850 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/advantage_air/translations/hu.json b/homeassistant/components/advantage_air/translations/hu.json new file mode 100644 index 00000000000..0da6d0d5304 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm", + "port": "Port" + }, + "description": "Csatlakozzon az Advantage Air fali t\u00e1blag\u00e9p API-j\u00e1hoz.", + "title": "Csatlakoz\u00e1s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index 45918735010..1d28556ba1a 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 655060337e1..53ab734e505 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt." }, "step": { @@ -15,6 +16,11 @@ "data": { "password": "Jelsz\u00f3" } + }, + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } } } } diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json new file mode 100644 index 00000000000..2d5f91cf373 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "create_entry": { + "default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json new file mode 100644 index 00000000000..19f706be1c8 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "Sikeres autentik\u00e1ci\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json new file mode 100644 index 00000000000..563ede56155 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 45918735010..1d28556ba1a 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/aurora/translations/hu.json b/homeassistant/components/aurora/translations/hu.json new file mode 100644 index 00000000000..d5363860cbd --- /dev/null +++ b/homeassistant/components/aurora/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni" + }, + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "K\u00fcsz\u00f6b (%)" + } + } + } + }, + "title": "Nemzeti \u00d3ce\u00e1n- \u00e9s L\u00e9gk\u00f6rkutat\u00e1si Hivatal (NOAA) Aurora \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/it.json b/homeassistant/components/aurora/translations/it.json new file mode 100644 index 00000000000..4350fbf555a --- /dev/null +++ b/homeassistant/components/aurora/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Soglia (%)" + } + } + } + }, + "title": "Sensore NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index bb33a36195d..659c50e49e7 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "error": { - "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "flow_title": "Axis eszk\u00f6z: {name} ({host})", "step": { @@ -14,5 +15,14 @@ } } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "V\u00e1lassza ki a haszn\u00e1lni k\u00edv\u00e1nt adatfolyam-profilt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index 436e8b1fb7d..6bd42409877 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "reauth": { + "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait." + } } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index bb4904f12dc..c4395ca806c 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -98,6 +98,10 @@ "off": "Norm\u00e1l", "on": "Alacsony" }, + "battery_charging": { + "off": "Nem t\u00f6lt\u0151dik", + "on": "T\u00f6lt\u0151dik" + }, "cold": { "off": "Norm\u00e1l", "on": "Hideg" @@ -122,6 +126,10 @@ "off": "Norm\u00e1l", "on": "Meleg" }, + "light": { + "off": "Nincs f\u00e9ny", + "on": "F\u00e9ny \u00e9szlelve" + }, "lock": { "off": "Bez\u00e1rva", "on": "Kinyitva" @@ -134,6 +142,10 @@ "off": "Norm\u00e1l", "on": "\u00c9szlelve" }, + "moving": { + "off": "Nincs mozg\u00e1sban", + "on": "Mozg\u00e1sban" + }, "occupancy": { "off": "Norm\u00e1l", "on": "\u00c9szlelve" @@ -142,6 +154,10 @@ "off": "Z\u00e1rva", "on": "Nyitva" }, + "plug": { + "off": "Kih\u00fazva", + "on": "Bedugva" + }, "presence": { "off": "T\u00e1vol", "on": "Otthon" diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index cbf055e2fba..a87786df1e8 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -1,6 +1,11 @@ { "config": { "step": { + "authorize": { + "data": { + "pin": "PIN k\u00f3d" + } + }, "user": { "data": { "host": "Hoszt" diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json index 19470d5a66d..65ee1f4db1d 100644 --- a/homeassistant/components/broadlink/translations/ru.json +++ b/homeassistant/components/broadlink/translations/ru.json @@ -25,7 +25,7 @@ "title": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "reset": { - "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Broadlink. \n 2. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \n 3. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443. \n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432\u043d\u0438\u0437.\n 5. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0437\u0430\u043c\u043e\u043a.", + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Broadlink. \n 2. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \n 3. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443. \n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432\u043d\u0438\u0437.\n 5. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443.", "title": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "unlock": { diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json index 2869d74fd04..dd5711cc516 100644 --- a/homeassistant/components/brother/translations/hu.json +++ b/homeassistant/components/brother/translations/hu.json @@ -5,6 +5,7 @@ "unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott." }, "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "snmp_error": "Az SNMP szerver ki van kapcsolva, vagy a nyomtat\u00f3 nem t\u00e1mogatott.", "wrong_host": "\u00c9rv\u00e9nytelen \u00e1llom\u00e1sn\u00e9v vagy IP-c\u00edm." }, diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index 45918735010..1d28556ba1a 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json index 22e9312e778..5bad24ecb6a 100644 --- a/homeassistant/components/cert_expiry/translations/hu.json +++ b/homeassistant/components/cert_expiry/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "import_failed": "Nem siker\u00fclt import\u00e1lni a konfigur\u00e1ci\u00f3t" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/climate/translations/ru.json b/homeassistant/components/climate/translations/ru.json index 3e4ff1844d6..4f8efaa5858 100644 --- a/homeassistant/components/climate/translations/ru.json +++ b/homeassistant/components/climate/translations/ru.json @@ -2,11 +2,11 @@ "device_automation": { "action_type": { "set_hvac_mode": "{entity_name}: \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b", - "set_preset_mode": "{entity_name}: \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443" + "set_preset_mode": "{entity_name}: \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0435\u0441\u0435\u0442" }, "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 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435" + "is_preset_mode": "{entity_name} \u0432 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u043c \u043f\u0440\u0435\u0441\u0435\u0442\u0435" }, "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", diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json new file mode 100644 index 00000000000..5dfc087c7bb --- /dev/null +++ b/homeassistant/components/cloud/translations/hu.json @@ -0,0 +1,11 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa enged\u00e9lyezve", + "can_reach_cloud_auth": "Hiteles\u00edt\u00e9si kiszolg\u00e1l\u00f3 el\u00e9r\u00e9se", + "google_enabled": "Google enged\u00e9lyezve", + "logged_in": "Bejelentkezve", + "subscription_expiration": "El\u0151fizet\u00e9s lej\u00e1rata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json new file mode 100644 index 00000000000..fa13d00617f --- /dev/null +++ b/homeassistant/components/cloudflare/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3", + "invalid_zone": "\u00c9rv\u00e9nytelen z\u00f3na" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "Rekordok" + }, + "title": "V\u00e1lassza a friss\u00edteni k\u00edv\u00e1nt rekordokat" + }, + "user": { + "data": { + "api_token": "API Token" + }, + "title": "Csatlakoz\u00e1s a Cloudflare szolg\u00e1ltat\u00e1shoz" + }, + "zone": { + "data": { + "zone": "Z\u00f3na" + }, + "title": "V\u00e1lassza ki a friss\u00edtend\u0151 z\u00f3n\u00e1t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index cbf055e2fba..cf688d6fdeb 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json index 149a1f713f4..ef589eb7f6d 100644 --- a/homeassistant/components/daikin/translations/hu.json +++ b/homeassistant/components/daikin/translations/hu.json @@ -3,9 +3,14 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba" + }, "step": { "user": { "data": { + "api_key": "API kulcs", "host": "Hoszt", "password": "Jelsz\u00f3" }, diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 3b2d79a34a7..aa56cb47741 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet" + }, + "error": { + "discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/hu.json b/homeassistant/components/device_tracker/translations/hu.json index 2954376e314..11c81f5e5ec 100644 --- a/homeassistant/components/device_tracker/translations/hu.json +++ b/homeassistant/components/device_tracker/translations/hu.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} otthon van", "is_not_home": "{entity_name} nincs otthon" + }, + "trigger_type": { + "enters": "{entity_name} bel\u00e9pett a z\u00f3n\u00e1ba", + "leaves": "{entity_name} elhagyta a z\u00f3n\u00e1t" } }, "state": { diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json new file mode 100644 index 00000000000..7a67a978ae1 --- /dev/null +++ b/homeassistant/components/dexcom/translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json index 638adb4ae12..04427a1efed 100644 --- a/homeassistant/components/dialogflow/translations/hu.json +++ b/homeassistant/components/dialogflow/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t] ( {dialogflow_url} ). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Dialogflowt?", diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json index 3b2d79a34a7..930b739fb18 100644 --- a/homeassistant/components/dsmr/translations/hu.json +++ b/homeassistant/components/dsmr/translations/hu.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minim\u00e1lis id\u0151 az entit\u00e1sfriss\u00edt\u00e9sek k\u00f6z\u00f6tt [mp]" + }, + "title": "DSMR opci\u00f3k" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index b295fb60747..9330c89a42a 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -11,5 +11,15 @@ "one": "uno", "other": "altri" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Tempo minimo tra gli aggiornamenti dell'entit\u00e0 [s]." + }, + "title": "Opzioni DSMR" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json index dcfcb155d12..3c69fd4562a 100644 --- a/homeassistant/components/elgato/translations/hu.json +++ b/homeassistant/components/elgato/translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van." + "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json new file mode 100644 index 00000000000..5ff60755bfd --- /dev/null +++ b/homeassistant/components/epson/translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Gazdag\u00e9p", + "name": "N\u00e9v", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json new file mode 100644 index 00000000000..a9c13f1ee68 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index 937f441845c..d13f5fa17c8 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json new file mode 100644 index 00000000000..563ede56155 --- /dev/null +++ b/homeassistant/components/guardian/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 981cb51c83a..4119802eb77 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,3 +1,17 @@ { + "system_health": { + "info": { + "disk_total": "\u00d6sszes hely", + "disk_used": "Felhaszn\u00e1lt hely", + "docker_version": "Docker verzi\u00f3", + "host_os": "Gazdag\u00e9p oper\u00e1ci\u00f3s rendszer", + "installed_addons": "Telep\u00edtett kieg\u00e9sz\u00edt\u0151k", + "supervisor_api": "Adminisztr\u00e1tor API", + "supervisor_version": "Adminisztr\u00e1tor verzi\u00f3", + "supported": "T\u00e1mogatott", + "update_channel": "Friss\u00edt\u00e9si csatorna", + "version_api": "API verzi\u00f3" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 981cb51c83a..937b6099bd9 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "Scheda di base", + "disk_total": "Disco totale", + "disk_used": "Disco utilizzato", + "docker_version": "Versione Docker", + "healthy": "Sano", + "host_os": "Sistema Operativo Host", + "installed_addons": "Componenti aggiuntivi installati", + "supervisor_api": "API Supervisore", + "supervisor_version": "Versione Supervisore", + "supported": "Supportato", + "update_channel": "Canale di aggiornamento", + "version_api": "Versione API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index cbf055e2fba..cf688d6fdeb 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json index 31804cfc421..f02fb97b9df 100644 --- a/homeassistant/components/home_connect/translations/hu.json +++ b/homeassistant/components/home_connect/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Sikeres autentik\u00e1ci\u00f3" + }, "step": { "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json new file mode 100644 index 00000000000..e202a747ac7 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "Processzor architekt\u00fara", + "chassis": "Kivitel", + "dev": "Fejleszt\u00e9s", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Adminisztr\u00e1tor", + "host_os": "Home Assistant OS", + "installation_type": "Telep\u00edt\u00e9s t\u00edpusa", + "os_name": "Oper\u00e1ci\u00f3s rendszer csal\u00e1d", + "os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja", + "python_version": "Python verzi\u00f3", + "supervisor": "Adminisztr\u00e1tor", + "timezone": "Id\u0151z\u00f3na", + "version": "Verzi\u00f3", + "virtualenv": "Virtu\u00e1lis k\u00f6rnyezet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 80f7b39d210..f3168807715 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -6,9 +6,10 @@ "dev": "Sviluppo", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisore", "host_os": "Sistema Operativo di Home Assistant", "installation_type": "Tipo di installazione", - "os_name": "Nome del Sistema Operativo", + "os_name": "Famiglia del Sistema Operativo", "os_version": "Versione del Sistema Operativo", "python_version": "Versione Python", "supervisor": "Supervisore", diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json new file mode 100644 index 00000000000..4cf1f44c439 --- /dev/null +++ b/homeassistant/components/homekit/translations/hu.json @@ -0,0 +1,17 @@ +{ + "options": { + "step": { + "include_exclude": { + "data": { + "entities": "Entit\u00e1sok", + "mode": "M\u00f3d" + } + }, + "init": { + "data": { + "mode": "M\u00f3d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index dee4ed9ee0f..149fee90583 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index e2664659d05..2e820418e94 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -5,6 +5,11 @@ "validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st" }, "step": { + "reauth": { + "data": { + "password": "Jelsz\u00f3" + } + }, "trusted_device": { "data": { "trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z" diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json index a64800ab7db..c3e7007b1a0 100644 --- a/homeassistant/components/ifttt/translations/hu.json +++ b/homeassistant/components/ifttt/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az \u201eIFTTT Webhook kisalkalmaz\u00e1s\u201d ( {applet_url} ) \"Webk\u00e9r\u00e9s k\u00e9sz\u00edt\u00e9se\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?", diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index f4bcbbf586e..396992156c0 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz, mert a kapcsolat friss\u00edt\u00e9se sz\u00fcks\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." }, "flow_title": "Nyomtat\u00f3: {name}", "step": { diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index a6a982dfdee..8516d609de1 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -22,7 +22,7 @@ }, "discovery_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 Kodi (`{name}`)?", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 Kodi" + "title": "Kodi" }, "user": { "data": { diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 327dd40e386..086e3ebf7d2 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "unknown": "V\u00e1ratlan hiba" + }, + "create_entry": { + "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3] ( {docs_url} )." + }, "error": { - "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", + "unknown": "V\u00e1ratlan hiba" }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json index a8c8bc05539..983ffacfa7d 100644 --- a/homeassistant/components/locative/translations/hu.json +++ b/homeassistant/components/locative/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?", diff --git a/homeassistant/components/lovelace/translations/hu.json b/homeassistant/components/lovelace/translations/hu.json new file mode 100644 index 00000000000..aa4934bad82 --- /dev/null +++ b/homeassistant/components/lovelace/translations/hu.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Ir\u00e1ny\u00edt\u00f3pultok", + "mode": "M\u00f3d", + "resources": "Er\u0151forr\u00e1sok", + "views": "N\u00e9zetek" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/it.json b/homeassistant/components/lovelace/translations/it.json index 918057460c0..b82bf3bff6a 100644 --- a/homeassistant/components/lovelace/translations/it.json +++ b/homeassistant/components/lovelace/translations/it.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Plance", "mode": "Modalit\u00e0", - "resources": "Risorse" + "resources": "Risorse", + "views": "Viste" } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index 2a3265e8c62..51bbe6ef04c 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks Mailgun-al] ( {mailgun_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Mailgunt?", diff --git a/homeassistant/components/meteo_france/translations/hu.json b/homeassistant/components/meteo_france/translations/hu.json index 83333c60fe8..dc74eafa409 100644 --- a/homeassistant/components/meteo_france/translations/hu.json +++ b/homeassistant/components/meteo_france/translations/hu.json @@ -4,6 +4,9 @@ "already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van", "unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb" }, + "error": { + "empty": "Nincs eredm\u00e9ny a v\u00e1roskeres\u00e9sben: ellen\u0151rizze a v\u00e1ros mez\u0151t" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 78778568e51..7a8958bd7c6 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van." }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 8bbf6ebc955..d9a216305e8 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -4,6 +4,9 @@ "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." }, + "create_entry": { + "default": "Sikeres autentik\u00e1ci\u00f3" + }, "error": { "internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json new file mode 100644 index 00000000000..8ac8f6d3b03 --- /dev/null +++ b/homeassistant/components/onewire/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_path": "A k\u00f6nyvt\u00e1r nem tal\u00e1lhat\u00f3." + }, + "step": { + "owserver": { + "data": { + "host": "Gazdag\u00e9p", + "port": "Port" + } + }, + "user": { + "data": { + "type": "Kapcsolat t\u00edpusa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index dfa0aec8765..c9c3f137984 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "auth": { "data": { @@ -7,11 +10,21 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } }, + "configure_profile": { + "data": { + "include": "Hozzon l\u00e9tre kamera entit\u00e1st" + }, + "description": "L\u00e9trehozza a(z) {profile} f\u00e9nyk\u00e9pez\u0151g\u00e9p entit\u00e1s\u00e1t {resolution} felbont\u00e1ssal?", + "title": "Profilok konfigur\u00e1l\u00e1sa" + }, "manual_input": { "data": { "host": "Hoszt", "port": "Port" } + }, + "user": { + "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban." } } } diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index f5481afa94a..c4b70e90076 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "error": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json new file mode 100644 index 00000000000..9729938035d --- /dev/null +++ b/homeassistant/components/ozw/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "addon_info_failed": "Nem siker\u00fclt bet\u00f6lteni az OpenZWave kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3kat.", + "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", + "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t." + }, + "error": { + "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." + }, + "step": { + "on_supervisor": { + "data": { + "use_addon": "Haszn\u00e1lja az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt" + }, + "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt?", + "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" + }, + "start_addon": { + "data": { + "network_key": "H\u00e1l\u00f3zati kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index 2c3a9a820f9..fbf7f49be6a 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json new file mode 100644 index 00000000000..76229e86224 --- /dev/null +++ b/homeassistant/components/plaato/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/hu.json b/homeassistant/components/profiler/translations/hu.json new file mode 100644 index 00000000000..bbdd2e5b536 --- /dev/null +++ b/homeassistant/components/profiler/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json index 7a8623b9030..c3bcabb0d3a 100644 --- a/homeassistant/components/ps4/translations/hu.json +++ b/homeassistant/components/ps4/translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a helyes-e." + }, "step": { "creds": { "title": "PlayStation 4" diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index be68eed63e3..44e24519ca2 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -10,5 +10,14 @@ "title": "T\u00f6ltsd ki az adataid" } } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "Alap\u00e9rtelmezett z\u00f3nafut\u00e1si id\u0151 (m\u00e1sodpercben)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/hu.json b/homeassistant/components/recollect_waste/translations/hu.json new file mode 100644 index 00000000000..112c8cb8385 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_place_or_service_id": "\u00c9rv\u00e9nytelen hely vagy szolg\u00e1ltat\u00e1s azonos\u00edt\u00f3" + }, + "step": { + "user": { + "data": { + "place_id": "Hely azonos\u00edt\u00f3ja", + "service_id": "Szolg\u00e1ltat\u00e1s azonos\u00edt\u00f3ja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/it.json b/homeassistant/components/recollect_waste/translations/it.json new file mode 100644 index 00000000000..d52e7be1282 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_place_or_service_id": "ID luogo o servizio non valido" + }, + "step": { + "user": { + "data": { + "place_id": "ID luogo", + "service_id": "ID servizio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 3b2d79a34a7..964b143f1d5 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" } + }, + "options": { + "step": { + "prompt_options": { + "data": { + "remove_device": "V\u00e1lassza ki a t\u00f6r\u00f6lni k\u00edv\u00e1nt eszk\u00f6zt" + }, + "title": "Rfxtrx opci\u00f3k" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/hu.json b/homeassistant/components/ruckus_unleashed/translations/hu.json new file mode 100644 index 00000000000..c1a23478ac4 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "Gazdag\u00e9p", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index 15eabd20d3d..ca42aff331a 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -4,6 +4,7 @@ "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.", "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.", "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." }, "flow_title": "Samsung TV: {model}", diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 5bb10e0f851..4258cfb0912 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" } } } \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json new file mode 100644 index 00000000000..b40828cc764 --- /dev/null +++ b/homeassistant/components/smarthab/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index a148bfa04fb..5cbe1d086bc 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -11,7 +11,8 @@ "pat": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" - } + }, + "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent] ( {token_url} ), amelyet az [utas\u00edt\u00e1sok] ( {component_url} ) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban." }, "user": { "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k] ({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json index 3df2fb30477..86927570c85 100644 --- a/homeassistant/components/somfy/translations/hu.json +++ b/homeassistant/components/somfy/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Sikeres autentik\u00e1ci\u00f3" + }, "step": { "pick_implementation": { "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index 23cb168b9a5..29e520de432 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "link": { "data": { diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json index 0b0581d4923..08a622fd238 100644 --- a/homeassistant/components/tibber/translations/hu.json +++ b/homeassistant/components/tibber/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" }, "step": { diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json new file mode 100644 index 00000000000..a14c446e673 --- /dev/null +++ b/homeassistant/components/traccar/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: \" {webhook_url} \" \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 97359a9a787..7c90b937329 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -1,14 +1,59 @@ { "config": { "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3" + }, + "flow_title": "Tuya konfigur\u00e1ci\u00f3", "step": { "user": { "data": { + "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)", "password": "Jelsz\u00f3", + "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", + "title": "Tuya" + } + } + }, + "options": { + "error": { + "dev_multi_type": "A konfigur\u00e1land\u00f3 eszk\u00f6z\u00f6knek azonos t\u00edpus\u00faaknak kell lennie", + "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3", + "dev_not_found": "Eszk\u00f6z nem tal\u00e1lhat\u00f3" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt f\u00e9nyer\u0151 tartom\u00e1ny", + "curr_temp_divider": "Aktu\u00e1lis h\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9k oszt\u00f3 (0 = alap\u00e9rtelmezetten)", + "max_kelvin": "Maxim\u00e1lis t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", + "max_temp": "Maxim\u00e1lis c\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (haszn\u00e1lja a min-t \u00e9s a max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)", + "min_kelvin": "Minimum t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", + "min_temp": "Min. C\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (alap\u00e9rtelmez\u00e9s szerint haszn\u00e1ljon min-t \u00e9s max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)", + "support_color": "Sz\u00ednt\u00e1mogat\u00e1s k\u00e9nyszer\u00edt\u00e9se", + "temp_divider": "Sz\u00ednh\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9kek oszt\u00f3ja (0 = alap\u00e9rtelmezett)", + "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet", + "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g" + }, + "description": "Konfigur\u00e1lja a(z) {device_type} eszk\u00f6zt \" {device_name} {device_type} \" megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", + "title": "Konfigur\u00e1lja a Tuya eszk\u00f6zt" + }, + "init": { + "data": { + "discovery_interval": "Felfedez\u0151 eszk\u00f6z lek\u00e9rdez\u00e9si intervalluma m\u00e1sodpercben", + "list_devices": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyja \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez", + "query_device": "V\u00e1lassza ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez", + "query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben" + }, + "description": "Ne \u00e1ll\u00edtsa t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", + "title": "Konfigur\u00e1lja a Tuya be\u00e1ll\u00edt\u00e1sokat" } } } diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json index f68f610bf2f..913e3d2a7a2 100644 --- a/homeassistant/components/twilio/translations/hu.json +++ b/homeassistant/components/twilio/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val] ( {twilio_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Twilio-t?", diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 91d031334dd..2a7a43d42e9 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -20,6 +20,9 @@ }, "options": { "step": { + "client_control": { + "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st." + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" diff --git a/homeassistant/components/upcloud/translations/hu.json b/homeassistant/components/upcloud/translations/hu.json new file mode 100644 index 00000000000..7a7de0633a7 --- /dev/null +++ b/homeassistant/components/upcloud/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z m\u00e1sodpercben, minimum 30" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/hu.json b/homeassistant/components/water_heater/translations/hu.json new file mode 100644 index 00000000000..c3c47030acb --- /dev/null +++ b/homeassistant/components/water_heater/translations/hu.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/hu.json b/homeassistant/components/wiffi/translations/hu.json new file mode 100644 index 00000000000..21320afea78 --- /dev/null +++ b/homeassistant/components/wiffi/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "start_server_failed": "A szerver ind\u00edt\u00e1sa nem siker\u00fclt." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index ed0cc9cdc1b..1486048adfc 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "A profil konfigur\u00e1ci\u00f3ja friss\u00edtve.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." }, diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index 40f6f151a2d..b89bd72f704 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van." + "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "flow_title": "WLED: {name}", "step": { diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json new file mode 100644 index 00000000000..19f706be1c8 --- /dev/null +++ b/homeassistant/components/xbox/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "Sikeres autentik\u00e1ci\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index 3b2d79a34a7..1a69e20c6b1 100644 --- a/homeassistant/components/xiaomi_aqara/translations/hu.json +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -1,7 +1,42 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut" + }, + "error": { + "discovery_error": "Nem siker\u00fclt felfedezni a Xiaomi Aqara K\u00f6zponti egys\u00e9get, pr\u00f3b\u00e1lja meg interf\u00e9szk\u00e9nt haszn\u00e1lni a HomeAssistant futtat\u00f3 eszk\u00f6z IP-j\u00e9t", + "invalid_host": " , l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "\u00c9rv\u00e9nytelen h\u00e1l\u00f3zati interf\u00e9sz", + "invalid_key": "\u00c9rv\u00e9nytelen kulcs", + "invalid_mac": "\u00c9rv\u00e9nytelen Mac-c\u00edm" + }, + "flow_title": "Xiaomi Aqara K\u00f6zponti egys\u00e9g: {name}", + "step": { + "select": { + "data": { + "select_ip": "IP c\u00edm" + }, + "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik K\u00f6zponti egys\u00e9get szeretne csatlakoztatni", + "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get" + }, + "settings": { + "data": { + "key": "K\u00f6zponti egys\u00e9g kulcsa", + "name": "K\u00f6zponti egys\u00e9g neve" + }, + "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k f\u00e9rhetnek hozz\u00e1", + "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g, opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok" + }, + "user": { + "data": { + "host": "IP c\u00edm (opcion\u00e1lis)", + "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz", + "mac": "Mac-c\u00edm (opcion\u00e1lis)" + }, + "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a mac-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g" + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index ef5ef6c0748..beb5c06c098 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -1,20 +1,30 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut" }, "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet." }, + "flow_title": "Xiaomi Miio: {name}", "step": { "gateway": { "data": { - "host": "IP c\u00edm" + "host": "IP c\u00edm", + "name": "K\u00f6zponti egys\u00e9g neve", + "token": "API Token" }, - "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token" + "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token", + "title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" }, "user": { - "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. " + "data": { + "gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" + }, + "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. ", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json index 3b2d79a34a7..10a03cebd21 100644 --- a/homeassistant/components/yeelight/translations/hu.json +++ b/homeassistant/components/yeelight/translations/hu.json @@ -1,7 +1,38 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "pick_device": { + "data": { + "device": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "host": "Gazdag\u00e9p" + }, + "description": "Ha a gazdag\u00e9pet \u00fcresen hagyja, felder\u00edt\u00e9sre ker\u00fcl automatikusan." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modell (opcion\u00e1lis)", + "nightlight_switch": "Haszn\u00e1lja az \u00c9jszakai kapcsol\u00f3t", + "save_on_change": "\u00c1llapot ment\u00e9se m\u00f3dos\u00edt\u00e1s ut\u00e1n", + "transition": "\u00c1tmeneti id\u0151 (ms)", + "use_music_mode": "Zene m\u00f3d enged\u00e9lyez\u00e9se" + }, + "description": "Ha modellt \u00fcresen hagyja, a rendszer automatikusan \u00e9rz\u00e9keli." + } } } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/hu.json b/homeassistant/components/zoneminder/translations/hu.json new file mode 100644 index 00000000000..f1f99fa2f7c --- /dev/null +++ b/homeassistant/components/zoneminder/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez." + }, + "create_entry": { + "default": "ZoneMinder szerver hozz\u00e1adva." + }, + "error": { + "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez." + } + } +} \ No newline at end of file From a6f47ac380cb42466f54f2c4ef0e90adb850d99a Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Sun, 15 Nov 2020 16:40:59 +0100 Subject: [PATCH 074/430] Remove yessssms integration (#43200) --- CODEOWNERS | 1 - homeassistant/components/yessssms/__init__.py | 1 - homeassistant/components/yessssms/const.py | 3 - .../components/yessssms/manifest.json | 7 - homeassistant/components/yessssms/notify.py | 101 ----- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/yessssms/__init__.py | 1 - tests/components/yessssms/test_notify.py | 367 ------------------ 9 files changed, 487 deletions(-) delete mode 100644 homeassistant/components/yessssms/__init__.py delete mode 100644 homeassistant/components/yessssms/const.py delete mode 100644 homeassistant/components/yessssms/manifest.json delete mode 100644 homeassistant/components/yessssms/notify.py delete mode 100644 tests/components/yessssms/__init__.py delete mode 100644 tests/components/yessssms/test_notify.py diff --git a/CODEOWNERS b/CODEOWNERS index bfb147158a6..215967c1c18 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -518,7 +518,6 @@ homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yandex_transport/* @rishatik92 @devbis homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn homeassistant/components/yeelightsunflower/* @lindsaymarkward -homeassistant/components/yessssms/* @flowolf homeassistant/components/yi/* @bachya homeassistant/components/zeroconf/* @bdraco homeassistant/components/zerproc/* @emlove diff --git a/homeassistant/components/yessssms/__init__.py b/homeassistant/components/yessssms/__init__.py deleted file mode 100644 index bc5f422ba75..00000000000 --- a/homeassistant/components/yessssms/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The yessssms component.""" diff --git a/homeassistant/components/yessssms/const.py b/homeassistant/components/yessssms/const.py deleted file mode 100644 index 473cdfff1e0..00000000000 --- a/homeassistant/components/yessssms/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Const for YesssSMS.""" - -CONF_PROVIDER = "provider" diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json deleted file mode 100644 index 5200408d1d5..00000000000 --- a/homeassistant/components/yessssms/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "yessssms", - "name": "yesss! SMS", - "documentation": "https://www.home-assistant.io/integrations/yessssms", - "requirements": ["YesssSMS==0.4.1"], - "codeowners": ["@flowolf"] -} diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py deleted file mode 100644 index 863602134a4..00000000000 --- a/homeassistant/components/yessssms/notify.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Support for the YesssSMS platform.""" -import logging - -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 .const import CONF_PROVIDER - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - 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.""" - - 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(), - ) - - _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, client, recipient): - """Initialize the service.""" - self.yesss = client - self._recipient = recipient - - def send_message(self, message="", **kwargs): - """Send a SMS message via Yesss.at's website.""" - if self.yesss.account_is_suspended(): - # only retry to login after Home Assistant was restarted with (hopefully) - # new login data. - _LOGGER.error( - "Account is suspended, cannot send SMS. " - "Check your login data and restart Home Assistant" - ) - return - try: - self.yesss.send(self._recipient, message) - except self.yesss.NoRecipientError as ex: - _LOGGER.error( - "You need to provide a recipient for SMS notification: %s", ex - ) - except self.yesss.EmptyMessageError as ex: - _LOGGER.error("Cannot send empty SMS message: %s", ex) - except self.yesss.SMSSendingError as ex: - _LOGGER.error(ex) - except self.yesss.ConnectionError as ex: - _LOGGER.error( - "Unable to connect to server of provider (%s): %s", - self.yesss.get_provider(), - ex, - ) - except self.yesss.AccountSuspendedError as ex: - _LOGGER.error( - "Wrong login credentials!! Verify correct credentials and " - "restart Home Assistant: %s", - ex, - ) - except self.yesss.LoginError as ex: - _LOGGER.error("Wrong login credentials: %s", ex) - else: - _LOGGER.info("SMS sent") diff --git a/requirements_all.txt b/requirements_all.txt index 32603901c8c..f8dc218d0ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -95,9 +95,6 @@ WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time WazeRouteCalculator==0.12 -# homeassistant.components.yessssms -YesssSMS==0.4.1 - # homeassistant.components.abode abodepy==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a0284af15b..70de317aa45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -38,9 +38,6 @@ RtmAPI==0.7.2 # homeassistant.components.onvif WSDiscovery==2.0.0 -# homeassistant.components.yessssms -YesssSMS==0.4.1 - # homeassistant.components.abode abodepy==1.1.0 diff --git a/tests/components/yessssms/__init__.py b/tests/components/yessssms/__init__.py deleted file mode 100644 index bf8e562009b..00000000000 --- a/tests/components/yessssms/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the yessssms component.""" diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py deleted file mode 100644 index 742b983fa83..00000000000 --- a/tests/components/yessssms/test_notify.py +++ /dev/null @@ -1,367 +0,0 @@ -"""The tests for the notify yessssms platform.""" -import logging - -import pytest - -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, - HTTP_INTERNAL_SERVER_ERROR, -) -from homeassistant.setup import async_setup_component - -from tests.async_mock import patch - - -@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 initialize 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.""" - caplog.set_level(logging.DEBUG) - 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.""" - caplog.set_level(logging.DEBUG) - 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 '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 - ) - - -@pytest.fixture(name="yessssms") -def yessssms_init(): - """Set up things to be run when tests are started.""" - login = "06641234567" - passwd = "testpasswd" - recipient = "06501234567" - client = yessssms.YesssSMS(login, passwd) - return yessssms.YesssSMSNotificationService(client, recipient) - - -async def test_login_error(yessssms, requests_mock, caplog): - """Test login that fails.""" - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._login_url, - status_code=200, - text="BlaBlaBlaLogin nicht erfolgreichBlaBla", - ) - - message = "Testing YesssSMS platform :)" - - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - assert requests_mock.called is True - assert requests_mock.call_count == 1 - - -async def test_empty_message_error(yessssms, caplog): - """Test for an empty SMS message error.""" - message = "" - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - - for record in caplog.records: - if ( - record.levelname == "ERROR" - and record.name == "homeassistant.components.yessssms.notify" - ): - assert "Cannot send empty SMS message" in record.message - - -async def test_error_account_suspended(yessssms, requests_mock, caplog): - """Test login that fails after multiple attempts.""" - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._login_url, - status_code=200, - text="BlaBlaBlaLogin nicht erfolgreichBlaBla", - ) - - message = "Testing YesssSMS platform :)" - - yessssms.send_message(message) - assert requests_mock.called is True - assert requests_mock.call_count == 1 - - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._login_url, - status_code=200, - text="Wegen 3 ungültigen Login-Versuchen ist Ihr Account für " - "eine Stunde gesperrt.", - ) - - message = "Testing YesssSMS platform :)" - - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - assert requests_mock.called is True - assert requests_mock.call_count == 2 - - -async def test_error_account_suspended_2(yessssms, caplog): - """Test login that fails after multiple attempts.""" - message = "Testing YesssSMS platform :)" - # pylint: disable=protected-access - yessssms.yesss._suspended = True - - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - for record in caplog.records: - if ( - record.levelname == "ERROR" - and record.name == "homeassistant.components.yessssms.notify" - ): - assert "Account is suspended, cannot send SMS." in record.message - - -async def test_send_message(yessssms, requests_mock, caplog): - """Test send message.""" - message = "Testing YesssSMS platform :)" - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._login_url, - status_code=302, - # pylint: disable=protected-access - headers={"location": yessssms.yesss._kontomanager}, - ) - # pylint: disable=protected-access - login = yessssms.yesss._logindata["login_rufnummer"] - requests_mock.get( - # pylint: disable=protected-access - yessssms.yesss._kontomanager, - status_code=200, - text=f"test...{login}", - ) - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._websms_url, - status_code=200, - text="

Ihre SMS wurde erfolgreich verschickt!

", - ) - requests_mock.get( - # pylint: disable=protected-access - yessssms.yesss._logout_url, - status_code=200, - ) - - with caplog.at_level(logging.INFO): - yessssms.send_message(message) - for record in caplog.records: - if ( - record.levelname == "INFO" - and record.name == "homeassistant.components.yessssms.notify" - ): - assert "SMS sent" in record.message - - assert requests_mock.called is True - assert requests_mock.call_count == 4 - assert ( - requests_mock.last_request.scheme - + "://" - + requests_mock.last_request.hostname - + requests_mock.last_request.path - + "?" - + requests_mock.last_request.query - ) in yessssms.yesss._logout_url # pylint: disable=protected-access - - -async def test_no_recipient_error(yessssms, caplog): - """Test for missing/empty recipient.""" - message = "Testing YesssSMS platform :)" - # pylint: disable=protected-access - yessssms._recipient = "" - - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - for record in caplog.records: - if ( - record.levelname == "ERROR" - and record.name == "homeassistant.components.yessssms.notify" - ): - assert ( - "You need to provide a recipient for SMS notification" in record.message - ) - - -async def test_sms_sending_error(yessssms, requests_mock, caplog): - """Test sms sending error.""" - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._login_url, - status_code=302, - # pylint: disable=protected-access - headers={"location": yessssms.yesss._kontomanager}, - ) - # pylint: disable=protected-access - login = yessssms.yesss._logindata["login_rufnummer"] - requests_mock.get( - # pylint: disable=protected-access - yessssms.yesss._kontomanager, - status_code=200, - text=f"test...{login}", - ) - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._websms_url, - status_code=HTTP_INTERNAL_SERVER_ERROR, - ) - - message = "Testing YesssSMS platform :)" - - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - - assert requests_mock.called is True - assert requests_mock.call_count == 3 - for record in caplog.records: - if ( - record.levelname == "ERROR" - and record.name == "homeassistant.components.yessssms.notify" - ): - assert "YesssSMS: error sending SMS" in record.message - - -async def test_connection_error(yessssms, requests_mock, caplog): - """Test connection error.""" - requests_mock.post( - # pylint: disable=protected-access - yessssms.yesss._login_url, - exc=yessssms.yesss.ConnectionError, - ) - - message = "Testing YesssSMS platform :)" - - with caplog.at_level(logging.ERROR): - yessssms.send_message(message) - - assert requests_mock.called is True - assert requests_mock.call_count == 1 - for record in caplog.records: - if ( - record.levelname == "ERROR" - and record.name == "homeassistant.components.yessssms.notify" - ): - assert "cannot connect to provider" in record.message From eb9e9e67f0dd92f248cfe1bb1721df9ab1fd4f01 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sun, 15 Nov 2020 12:06:51 -0500 Subject: [PATCH 075/430] Update greeclimate to 0.10.3 (#43248) * Update greeclimate to 0.10.3 * Device search needs to be mocked in tests --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/test_init.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 2d2d293085b..0d2bed3ff28 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,6 +3,6 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.10.2"], + "requirements": ["greeclimate==0.10.3"], "codeowners": ["@cmroche"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index f8dc218d0ee..2db4604d503 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -702,7 +702,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.10.2 +greeclimate==0.10.3 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70de317aa45..184425caaa4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -361,7 +361,7 @@ google-cloud-pubsub==2.1.0 google-nest-sdm==0.1.14 # homeassistant.components.gree -greeclimate==0.10.2 +greeclimate==0.10.3 # homeassistant.components.griddy griddypower==0.1.0 diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index 1ea0727b220..ef693c9538a 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -8,7 +8,7 @@ from tests.async_mock import patch from tests.common import MockConfigEntry -async def test_setup_simple(hass): +async def test_setup_simple(hass, discovery, device): """Test gree integration is setup.""" await async_setup_component(hass, GREE_DOMAIN, {}) await hass.async_block_till_done() @@ -17,7 +17,7 @@ async def test_setup_simple(hass): assert len(hass.config_entries.flow.async_progress()) == 0 -async def test_unload_config_entry(hass): +async def test_unload_config_entry(hass, discovery, device): """Test that the async_unload_entry works.""" # As we have currently no configuration, we just to pass the domain here. entry = MockConfigEntry(domain=GREE_DOMAIN) From 869cb83170ca89dea0ccc1d12c0ac47f2ec0ff4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Nov 2020 07:40:03 -1000 Subject: [PATCH 076/430] Set should_poll for zone entities (#43212) --- homeassistant/components/zone/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index c3e1beb44af..01a8b9aa0f4 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -325,6 +325,11 @@ class Zone(entity.Entity): """Return the state attributes of the zone.""" return self._attrs + @property + def should_poll(self) -> bool: + """Zone does not poll.""" + return False + async def async_update_config(self, config: Dict) -> None: """Handle when the config is updated.""" if self._config == config: From 65b2ef659ea8cfb62c250bce9778a558dd2ab690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 15 Nov 2020 18:40:45 +0100 Subject: [PATCH 077/430] Update zigpy-zigate to 0.7.2 (#43252) * update zigpy_zigate to v0.7.1 * bump zigpy-zigate to 0.7.2 --- 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 0a42b6b357e..0dfb9ab5098 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.11.0", "zigpy==0.27.0", "zigpy-xbee==0.13.0", - "zigpy-zigate==0.7.1", + "zigpy-zigate==0.7.2", "zigpy-znp==0.2.2" ], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 2db4604d503..56027b97ae6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2353,7 +2353,7 @@ zigpy-deconz==0.11.0 zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.7.1 +zigpy-zigate==0.7.2 # homeassistant.components.zha zigpy-znp==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 184425caaa4..0df8aa39764 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ zigpy-deconz==0.11.0 zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.7.1 +zigpy-zigate==0.7.2 # homeassistant.components.zha zigpy-znp==0.2.2 From 61475cf090854b42421f726ec0653d5b50fa9a6e Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Sun, 15 Nov 2020 20:42:11 +0000 Subject: [PATCH 078/430] Support gas meter capability for smartthings (#41310) --- homeassistant/components/smartthings/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index f0240886913..e07424520d2 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, VOLT, + VOLUME_CUBIC_METERS, ) from . import SmartThingsEntity @@ -116,6 +117,12 @@ CAPABILITY_TO_SENSORS = { None, ) ], + Capability.gas_meter: [ + Map(Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, None), + Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None), + Map(Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP), + Map(Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, None), + ], Capability.illuminance_measurement: [ Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE) ], From 149ba088d4a18f7e32929a2f79ba2013fff0906f Mon Sep 17 00:00:00 2001 From: Tim de Boer Date: Sun, 15 Nov 2020 21:52:31 +0100 Subject: [PATCH 079/430] Add dsmr_reader telegram timestamp and device classes (#42909) * Added Telegram timestamp * added to 'dsmr/reading/timestamp' and 'dsmr/consumption/gas/read_at' * Fixed import sorting * Replaced 'kW' with ENERGY_KILO_WATT_HOUR * Added device_class, changed unit_of_measurement with fallback on device_class * Fixed typo * Fixed 'black' coding format * Removed fallback, added unit_of_measurement and CURRENCY_EURO as device_class * Fixed newline * Removed 'timestamp' unit_of_meassure * Removed icons from defintions with device_class * Updated device_classes * Updated device_classes * Updated device_classes * Added 'entity_registry_enabled_default' properties * Added 'entity_registry_enabled_default' properties, fixed typo * MQTT discovery will be in another pull-request --- .../components/dsmr_reader/definitions.py | 128 +++++++++++++----- .../components/dsmr_reader/sensor.py | 12 ++ 2 files changed, 106 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 5fda67e65a3..309f0d297ec 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -2,8 +2,14 @@ from homeassistant.const import ( CURRENCY_EURO, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, + POWER_KILO_WATT, VOLT, VOLUME_CUBIC_METERS, ) @@ -26,243 +32,297 @@ def tariff_transform(value): DEFINITIONS = { "dsmr/reading/electricity_delivered_1": { "name": "Low tariff usage", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/reading/electricity_returned_1": { "name": "Low tariff returned", - "icon": "mdi:flash-outline", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/reading/electricity_delivered_2": { "name": "High tariff usage", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/reading/electricity_returned_2": { "name": "High tariff returned", - "icon": "mdi:flash-outline", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/reading/electricity_currently_delivered": { "name": "Current power usage", - "icon": "mdi:flash", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/electricity_currently_returned": { "name": "Current power return", - "icon": "mdi:flash-outline", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/phase_currently_delivered_l1": { "name": "Current power usage L1", - "icon": "mdi:flash", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/phase_currently_delivered_l2": { "name": "Current power usage L2", - "icon": "mdi:flash", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/phase_currently_delivered_l3": { "name": "Current power usage L3", - "icon": "mdi:flash", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/phase_currently_returned_l1": { "name": "Current power return L1", - "icon": "mdi:flash-outline", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/phase_currently_returned_l2": { "name": "Current power return L2", - "icon": "mdi:flash-outline", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/phase_currently_returned_l3": { "name": "Current power return L3", - "icon": "mdi:flash-outline", - "unit": "kW", + "enable_default": True, + "device_class": DEVICE_CLASS_POWER, + "unit": POWER_KILO_WATT, }, "dsmr/reading/extra_device_delivered": { "name": "Gas meter usage", + "enable_default": True, "icon": "mdi:fire", "unit": VOLUME_CUBIC_METERS, }, "dsmr/reading/phase_voltage_l1": { "name": "Current voltage L1", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_VOLTAGE, "unit": VOLT, }, "dsmr/reading/phase_voltage_l2": { "name": "Current voltage L2", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_VOLTAGE, "unit": VOLT, }, "dsmr/reading/phase_voltage_l3": { "name": "Current voltage L3", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_VOLTAGE, "unit": VOLT, }, "dsmr/reading/phase_power_current_l1": { "name": "Phase power current L1", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_CURRENT, "unit": ELECTRICAL_CURRENT_AMPERE, }, "dsmr/reading/phase_power_current_l2": { "name": "Phase power current L2", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_CURRENT, "unit": ELECTRICAL_CURRENT_AMPERE, }, "dsmr/reading/phase_power_current_l3": { "name": "Phase power current L3", - "icon": "mdi:flash", + "enable_default": True, + "device_class": DEVICE_CLASS_CURRENT, "unit": ELECTRICAL_CURRENT_AMPERE, }, + "dsmr/reading/timestamp": { + "name": "Telegram timestamp", + "enable_default": False, + "device_class": DEVICE_CLASS_TIMESTAMP, + }, "dsmr/consumption/gas/delivered": { "name": "Gas usage", + "enable_default": True, "icon": "mdi:fire", "unit": VOLUME_CUBIC_METERS, }, "dsmr/consumption/gas/currently_delivered": { "name": "Current gas usage", + "enable_default": True, "icon": "mdi:fire", "unit": VOLUME_CUBIC_METERS, }, "dsmr/consumption/gas/read_at": { "name": "Gas meter read", - "icon": "mdi:clock", - "unit": "", + "enable_default": True, + "device_class": DEVICE_CLASS_TIMESTAMP, }, "dsmr/day-consumption/electricity1": { "name": "Low tariff usage", - "icon": "mdi:counter", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/day-consumption/electricity2": { "name": "High tariff usage", - "icon": "mdi:counter", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/day-consumption/electricity1_returned": { "name": "Low tariff return", - "icon": "mdi:counter", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/day-consumption/electricity2_returned": { "name": "High tariff return", - "icon": "mdi:counter", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/day-consumption/electricity_merged": { "name": "Power usage total", - "icon": "mdi:counter", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/day-consumption/electricity_returned_merged": { "name": "Power return total", - "icon": "mdi:counter", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, "unit": ENERGY_KILO_WATT_HOUR, }, "dsmr/day-consumption/electricity1_cost": { "name": "Low tariff cost", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/electricity2_cost": { "name": "High tariff cost", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/electricity_cost_merged": { "name": "Power total cost", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/gas": { "name": "Gas usage", + "enable_default": True, "icon": "mdi:counter", "unit": VOLUME_CUBIC_METERS, }, "dsmr/day-consumption/gas_cost": { "name": "Gas cost", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/total_cost": { "name": "Total cost", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { "name": "Low tariff delivered price", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { "name": "High tariff delivered price", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { "name": "Low tariff returned price", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { "name": "High tariff returned price", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_gas": { "name": "Gas price", + "enable_default": True, "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, "dsmr/meter-stats/dsmr_version": { "name": "DSMR version", + "enable_default": True, "icon": "mdi:alert-circle", "transform": dsmr_transform, }, "dsmr/meter-stats/electricity_tariff": { "name": "Electricity tariff", + "enable_default": True, "icon": "mdi:flash", "transform": tariff_transform, }, "dsmr/meter-stats/power_failure_count": { "name": "Power failure count", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/long_power_failure_count": { "name": "Long power failure count", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/voltage_sag_count_l1": { "name": "Voltage sag L1", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/voltage_sag_count_l2": { "name": "Voltage sag L2", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/voltage_sag_count_l3": { "name": "Voltage sag L3", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/voltage_swell_count_l1": { "name": "Voltage swell L1", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/voltage_swell_count_l2": { "name": "Voltage swell L2", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/voltage_swell_count_l3": { "name": "Voltage swell L3", + "enable_default": True, "icon": "mdi:flash", }, "dsmr/meter-stats/rejected_telegrams": { "name": "Rejected telegrams", + "enable_default": True, "icon": "mdi:flash", }, } diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 341451522d4..14234b49dbe 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -31,6 +31,8 @@ class DSMRSensor(Entity): self._topic = topic self._name = self._definition.get("name", topic.split("/")[-1]) + self._device_class = self._definition.get("device_class") + self._enable_default = self._definition.get("enable_default") self._unit_of_measurement = self._definition.get("unit") self._icon = self._definition.get("icon") self._transform = self._definition.get("transform") @@ -67,11 +69,21 @@ class DSMRSensor(Entity): """Return the current state of the entity.""" return self._state + @property + def device_class(self): + """Return the device_class of this sensor.""" + return self._device_class + @property def unit_of_measurement(self): """Return the unit_of_measurement of this sensor.""" return self._unit_of_measurement + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enable_default + @property def icon(self): """Return the icon of this sensor.""" From 9d0cd9c1b10cd90bb2397d9f493a53d51a4012bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Nov 2020 11:18:23 -1000 Subject: [PATCH 080/430] Convert most esphome state updates to callbacks (#43246) --- homeassistant/components/esphome/__init__.py | 64 +++++++++----------- homeassistant/components/esphome/camera.py | 25 ++++++-- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index c9d07a22ec6..a12754a87f4 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -489,58 +489,38 @@ def esphome_map_enum(func: Callable[[], Dict[int, str]]): return EsphomeEnumMapper(func) -class EsphomeEntity(Entity): - """Define a generic esphome entity.""" +class EsphomeBaseEntity(Entity): + """Define a base esphome entity.""" def __init__(self, entry_id: str, component_key: str, key: int): """Initialize.""" self._entry_id = entry_id self._component_key = component_key self._key = key - self._remove_callbacks: List[Callable[[], None]] = [] async def async_added_to_hass(self) -> None: """Register callbacks.""" - kwargs = { - "entry_id": self._entry_id, - "component_key": self._component_key, - "key": self._key, - } - self._remove_callbacks.append( + self.async_on_remove( async_dispatcher_connect( self.hass, ( - f"esphome_{kwargs.get('entry_id')}" - f"_update_{kwargs.get('component_key')}_{kwargs.get('key')}" - ), - self._on_state_update, - ) - ) - - self._remove_callbacks.append( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{kwargs.get('entry_id')}_remove_" - f"{kwargs.get('component_key')}_{kwargs.get('key')}" + f"esphome_{self._entry_id}_remove_" + f"{self._component_key}_{self._key}" ), self.async_remove, ) ) - self._remove_callbacks.append( + self.async_on_remove( async_dispatcher_connect( self.hass, - f"esphome_{kwargs.get('entry_id')}_on_device_update", + f"esphome_{self._entry_id}_on_device_update", self._on_device_update, ) ) - async def _on_state_update(self) -> None: - """Update the entity state when state or static info changed.""" - self.async_write_ha_state() - - async def _on_device_update(self) -> None: + @callback + 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. @@ -549,12 +529,6 @@ class EsphomeEntity(Entity): return self.async_write_ha_state() - async def async_will_remove_from_hass(self) -> None: - """Unregister callbacks.""" - for remove_callback in self._remove_callbacks: - remove_callback() - self._remove_callbacks = [] - @property def _entry_data(self) -> RuntimeEntryData: return self.hass.data[DATA_KEY][self._entry_id] @@ -619,3 +593,23 @@ class EsphomeEntity(Entity): def should_poll(self) -> bool: """Disable polling.""" return False + + +class EsphomeEntity(EsphomeBaseEntity): + """Define a generic esphome entity.""" + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ( + f"esphome_{self._entry_id}" + f"_update_{self._component_key}_{self._key}" + ), + self.async_write_ha_state, + ) + ) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 1678281c3de..5b8f4f0d7e6 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -7,9 +7,10 @@ from aioesphomeapi import CameraInfo, CameraState from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from . import EsphomeEntity, platform_async_setup_entry +from . import EsphomeBaseEntity, platform_async_setup_entry async def async_setup_entry( @@ -27,13 +28,13 @@ async def async_setup_entry( ) -class EsphomeCamera(Camera, EsphomeEntity): +class EsphomeCamera(Camera, EsphomeBaseEntity): """A camera implementation for ESPHome.""" def __init__(self, entry_id: str, component_key: str, key: int): """Initialize.""" Camera.__init__(self) - EsphomeEntity.__init__(self, entry_id, component_key, key) + EsphomeBaseEntity.__init__(self, entry_id, component_key, key) self._image_cond = asyncio.Condition() @property @@ -44,9 +45,25 @@ class EsphomeCamera(Camera, EsphomeEntity): def _state(self) -> Optional[CameraState]: return super()._state + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ( + f"esphome_{self._entry_id}" + f"_update_{self._component_key}_{self._key}" + ), + self._on_state_update, + ) + ) + async def _on_state_update(self) -> None: """Notify listeners of new image when update arrives.""" - await super()._on_state_update() + self.async_write_ha_state() async with self._image_cond: self._image_cond.notify_all() From 7dcfc8f1fa462f4372374312c75f24eab0cd81e7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 16 Nov 2020 00:03:40 +0000 Subject: [PATCH 081/430] [ci skip] Translation update --- homeassistant/components/acmeda/translations/no.json | 2 +- .../components/aurora/translations/zh-Hans.json | 11 +++++++++++ .../components/binary_sensor/translations/ru.json | 4 ++++ homeassistant/components/cloud/translations/ru.json | 3 +++ .../components/flick_electric/translations/no.json | 2 +- homeassistant/components/flume/translations/no.json | 2 +- homeassistant/components/hassio/translations/no.json | 2 +- .../components/hassio/translations/zh-Hans.json | 5 +++++ .../components/homematicip_cloud/translations/no.json | 2 +- .../components/opentherm_gw/translations/no.json | 2 +- .../recollect_waste/translations/zh-Hans.json | 11 +++++++++++ .../components/solaredge/translations/no.json | 2 +- .../components/speedtestdotnet/translations/no.json | 2 +- .../components/starline/translations/no.json | 2 +- homeassistant/components/tradfri/translations/no.json | 2 +- homeassistant/components/unifi/translations/no.json | 2 +- homeassistant/components/vera/translations/no.json | 8 ++++---- 17 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/aurora/translations/zh-Hans.json create mode 100644 homeassistant/components/recollect_waste/translations/zh-Hans.json diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json index 51eb5668bc9..45d764e8112 100644 --- a/homeassistant/components/acmeda/translations/no.json +++ b/homeassistant/components/acmeda/translations/no.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "id": "Verts-ID" + "id": "Vert ID" }, "title": "Velg en hub du vil legge til" } diff --git a/homeassistant/components/aurora/translations/zh-Hans.json b/homeassistant/components/aurora/translations/zh-Hans.json new file mode 100644 index 00000000000..e28e3121f38 --- /dev/null +++ b/homeassistant/components/aurora/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u540d\u79f0" + } + } + } + } +} \ 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 6b80fafe5dd..6cb5e4a8b56 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -146,6 +146,10 @@ "off": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", "on": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e" }, + "plug": { + "off": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, "presence": { "off": "\u041d\u0435 \u0434\u043e\u043c\u0430", "on": "\u0414\u043e\u043c\u0430" diff --git a/homeassistant/components/cloud/translations/ru.json b/homeassistant/components/cloud/translations/ru.json index b66e2ca51fa..b2d8c55369b 100644 --- a/homeassistant/components/cloud/translations/ru.json +++ b/homeassistant/components/cloud/translations/ru.json @@ -7,6 +7,9 @@ "can_reach_cloud_auth": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438", "google_enabled": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Google", "logged_in": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443", + "relayer_connected": "Relayer \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d", + "remote_connected": "\u0423\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d", + "remote_enabled": "\u0423\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d", "subscription_expiration": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438" } } diff --git a/homeassistant/components/flick_electric/translations/no.json b/homeassistant/components/flick_electric/translations/no.json index 5cc6357058d..21f0b0fabc8 100644 --- a/homeassistant/components/flick_electric/translations/no.json +++ b/homeassistant/components/flick_electric/translations/no.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "client_id": "Klient-ID (valgfritt)", + "client_id": "Klient ID (valgfritt)", "client_secret": "Klienthemmelighet (valgfritt)", "password": "Passord", "username": "Brukernavn" diff --git a/homeassistant/components/flume/translations/no.json b/homeassistant/components/flume/translations/no.json index 78dd7e40217..5f473bfdfef 100644 --- a/homeassistant/components/flume/translations/no.json +++ b/homeassistant/components/flume/translations/no.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "client_id": "Klient-ID", + "client_id": "Klient ID", "client_secret": "Klienthemmelighet", "password": "Passord", "username": "Brukernavn" diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 2fb04f5156e..e652f76a12c 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -8,7 +8,7 @@ "healthy": "Sunn", "host_os": "Vertsoperativsystem", "installed_addons": "Installerte tillegg", - "supervisor_api": "API for Supervisor", + "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor versjon", "supported": "St\u00f8ttet", "update_channel": "Oppdater kanal", diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json index 981cb51c83a..23af6c3885e 100644 --- a/homeassistant/components/hassio/translations/zh-Hans.json +++ b/homeassistant/components/hassio/translations/zh-Hans.json @@ -1,3 +1,8 @@ { + "system_health": { + "info": { + "version_api": "API\u7248\u672c" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/no.json b/homeassistant/components/homematicip_cloud/translations/no.json index ef171c7b52e..d28fe17a691 100644 --- a/homeassistant/components/homematicip_cloud/translations/no.json +++ b/homeassistant/components/homematicip_cloud/translations/no.json @@ -14,7 +14,7 @@ "step": { "init": { "data": { - "hapid": "Tilgangspunkt-ID (SGTIN)", + "hapid": "Tilgangspunkt ID (SGTIN)", "name": "Navn (valgfritt, brukes som navneprefiks for alle enheter)", "pin": "PIN-kode" }, diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 7789573a42e..76118924e0a 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -3,7 +3,7 @@ "error": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "id_exists": "Gateway-ID finnes allerede" + "id_exists": "Gateway ID finnes allerede" }, "step": { "init": { diff --git a/homeassistant/components/recollect_waste/translations/zh-Hans.json b/homeassistant/components/recollect_waste/translations/zh-Hans.json new file mode 100644 index 00000000000..f4ba15680e4 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "service_id": "\u670d\u52a1 ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/no.json b/homeassistant/components/solaredge/translations/no.json index 5360bd6d810..c6dc9489385 100644 --- a/homeassistant/components/solaredge/translations/no.json +++ b/homeassistant/components/solaredge/translations/no.json @@ -11,7 +11,7 @@ "data": { "api_key": "API-n\u00f8kkel", "name": "Navnet p\u00e5 denne installasjonen", - "site_id": "SolarEdge nettsted-id" + "site_id": "SolarEdge nettsted ID" }, "title": "Definer API-parametrene for denne installasjonen" } diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json index 98a372f81dc..a079bc1fa7b 100644 --- a/homeassistant/components/speedtestdotnet/translations/no.json +++ b/homeassistant/components/speedtestdotnet/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "wrong_server_id": "Server-ID er ikke gyldig" + "wrong_server_id": "Server ID er ikke gyldig" }, "step": { "user": { diff --git a/homeassistant/components/starline/translations/no.json b/homeassistant/components/starline/translations/no.json index 36545f3efd7..d6d6acb39d3 100644 --- a/homeassistant/components/starline/translations/no.json +++ b/homeassistant/components/starline/translations/no.json @@ -8,7 +8,7 @@ "step": { "auth_app": { "data": { - "app_id": "App-ID", + "app_id": "", "app_secret": "Hemmelig" }, "description": "Applikasjons-ID og hemmelig kode fra [StarLine utviklerkonto](https://my.starline.ru/developer)", diff --git a/homeassistant/components/tradfri/translations/no.json b/homeassistant/components/tradfri/translations/no.json index 917a6587b84..abdf0a26b12 100644 --- a/homeassistant/components/tradfri/translations/no.json +++ b/homeassistant/components/tradfri/translations/no.json @@ -15,7 +15,7 @@ "host": "Vert", "security_code": "Sikkerhetskode" }, - "description": "Du finner sikkerhetskoden p\u00e5 baksiden av gatewayen din.", + "description": "Du finner sikkerhetskoden p\u00e5 baksiden av gatewayen din", "title": "Angi sikkerhetskode" } } diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index 58af2bd5e54..5cda9ad7ab5 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -14,7 +14,7 @@ "host": "Vert", "password": "Passord", "port": "Port", - "site": "Nettsted-ID", + "site": "Nettsted ID", "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" }, diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json index ae1a601d550..7ec6850a7c8 100644 --- a/homeassistant/components/vera/translations/no.json +++ b/homeassistant/components/vera/translations/no.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "exclude": "Vera-enhets-ID-er som skal ekskluderes fra Home Assistant.", - "lights": "Vera bytter enhets-ID-er for \u00e5 behandle som lys i Home Assistant.", + "exclude": "Vera enhets ID-er som skal ekskluderes fra Home Assistant", + "lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant", "vera_controller_url": "URL-adresse for kontroller" }, "description": "Oppgi en Vera-kontroller-url nedenfor. Det skal se slik ut: http://192.168.1.161:3480.", @@ -19,8 +19,8 @@ "step": { "init": { "data": { - "exclude": "Vera-enhets-ID-er som skal ekskluderes fra Home Assistant.", - "lights": "Vera bytter enhets-ID-er for \u00e5 behandle som lys i Home Assistant." + "exclude": "Vera enhets ID-er som skal ekskluderes fra Home Assistant", + "lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant" }, "description": "Se Vera dokumentasjonen for detaljer om valgfrie parametere: https://www.home-assistant.io/integrations/vera/. Merk: Eventuelle endringer her vil trenge en omstart av Home Assistant-serveren. For \u00e5 fjerne verdier, gi et rom.", "title": "Alternativer for Vera-kontroller" From 467d79c7fdfa44337e9a64776e999fe590ace4b4 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 16 Nov 2020 00:49:41 -0600 Subject: [PATCH 082/430] Add tests for browse media image proxy (#43076) * add tests for browse media image proxy * Update test_init.py * Update __init__.py * Update __init__.py * Update __init__.py * Update __init__.py --- .../components/media_player/__init__.py | 16 +++++++---- tests/components/media_player/test_init.py | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 71db60baa2e..d670acb7af9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,7 +7,7 @@ import functools as ft import hashlib import logging import secrets -from typing import List, Optional +from typing import List, Optional, Tuple from urllib.parse import urlparse from aiohttp import web @@ -434,8 +434,11 @@ class MediaPlayerEntity(Entity): return await self._async_fetch_image_from_cache(url) async def async_get_browse_image( - self, media_content_type, media_content_id, media_image_id=None - ): + self, + media_content_type: str, + media_content_id: str, + media_image_id: Optional[str] = None, + ) -> Tuple[Optional[str], Optional[str]]: """ Optionally fetch internally accessible image for media browser. @@ -906,8 +909,11 @@ class MediaPlayerEntity(Entity): return content, content_type def get_browse_image_url( - self, media_content_type, media_content_id, media_image_id=None - ): + self, + media_content_type: str, + media_content_id: str, + media_image_id: Optional[str] = None, + ) -> str: """Generate an url for a media browser image.""" url_path = ( f"/api/media_player_proxy/{self.entity_id}/browse_media" diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 02012a1f71d..9434fb1a411 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -92,6 +92,33 @@ async def test_get_image_http_remote(hass, aiohttp_client): assert content == b"image" +async def test_get_async_get_browse_image(hass, aiohttp_client, hass_ws_client): + """Test get browse image.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + entity_comp = hass.data.get("entity_components", {}).get("media_player") + assert entity_comp + + player = entity_comp.get_entity("media_player.bedroom") + assert player + + client = await aiohttp_client(hass.http.app) + + with patch( + "homeassistant.components.media_player.MediaPlayerEntity." + "async_get_browse_image", + return_value=(b"image", "image/jpeg"), + ): + url = player.get_browse_image_url("album", "abcd") + resp = await client.get(url) + content = await resp.read() + + assert content == b"image" + + def test_deprecated_base_class(caplog): """Test deprecated base class.""" From 60314ecc611b1632143ec90546637d5d28555ef1 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Nov 2020 11:18:48 +0100 Subject: [PATCH 083/430] Apply suggestions from #42697 to synology_dsm (#43197) * apply suggestions from #42697 * fix tests * use MockConfigEntry for test * use hass.config_entries.async_setup() * disable default fixture * rename marker to no_bypass_setup --- .../components/synology_dsm/__init__.py | 19 +++---- tests/components/synology_dsm/conftest.py | 18 +++++-- tests/components/synology_dsm/consts.py | 14 ++++++ .../synology_dsm/test_config_flow.py | 49 ++++++------------- tests/components/synology_dsm/test_init.py | 41 ++++++++++++++++ 5 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 tests/components/synology_dsm/consts.py create mode 100644 tests/components/synology_dsm/test_init.py diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index d8acf29016c..06696865d03 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -185,7 +185,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await api.async_setup() except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", str(err)) + _LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", err) raise ConfigEntryNotReady from err undo_listener = entry.add_update_listener(_async_update_listener) @@ -244,9 +244,6 @@ async def _async_setup_services(hass: HomeAssistantType): async def service_handler(call: ServiceCall): """Handle service call.""" - _LOGGER.debug( - "service_handler - called as '%s' with data: %s", call.service, call.data - ) serial = call.data.get(CONF_SERIAL) dsm_devices = hass.data[DOMAIN] @@ -268,7 +265,7 @@ async def _async_setup_services(hass: HomeAssistantType): ) return - _LOGGER.info("%s DSM with serial %s", call.service, serial) + _LOGGER.debug("%s DSM with serial %s", call.service, serial) dsm_api = dsm_device[SYNO_API] if call.service == SERVICE_REBOOT: await dsm_api.async_reboot() @@ -276,9 +273,6 @@ async def _async_setup_services(hass: HomeAssistantType): await dsm_api.system.shutdown() for service in SERVICES: - _LOGGER.debug( - "_async_setup_services - register service %s on domain %s", service, DOMAIN - ) hass.services.async_register(DOMAIN, service, service_handler) @@ -445,14 +439,14 @@ class SynoApi: if not self.system: _LOGGER.debug("async_reboot - System API not ready: %s", self) return - self._hass.async_add_executor_job(self.system.reboot) + await self._hass.async_add_executor_job(self.system.reboot) async def async_shutdown(self): """Shutdown NAS.""" if not self.system: _LOGGER.debug("async_shutdown - System API not ready: %s", self) return - self._hass.async_add_executor_job(self.system.shutdown) + await self._hass.async_add_executor_job(self.system.shutdown) async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" @@ -465,13 +459,14 @@ class SynoApi: await self._hass.async_add_executor_job( self.dsm.update, self._with_information ) - async_dispatcher_send(self._hass, self.signal_sensor_update) except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: _LOGGER.warning( "async_update - connection error during update, fallback by reloading the entry" ) - _LOGGER.debug("async_update - exception: %s", str(err)) + _LOGGER.debug("async_update - exception: %s", err) await self._hass.config_entries.async_reload(self._entry.entry_id) + return + async_dispatcher_send(self._hass, self.signal_sensor_update) class SynologyDSMEntity(Entity): diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 41bd42a98b3..db25bd59ada 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -4,10 +4,20 @@ import pytest from tests.async_mock import patch +def pytest_configure(config): + """Register custom marker for tests.""" + config.addinivalue_line( + "markers", "no_bypass_setup: mark test to disable bypass_setup_fixture" + ) + + @pytest.fixture(name="bypass_setup", autouse=True) -def bypass_setup_fixture(): +def bypass_setup_fixture(request): """Mock component setup.""" - with patch( - "homeassistant.components.synology_dsm.async_setup_entry", return_value=True - ): + if "no_bypass_setup" in request.keywords: yield + else: + with patch( + "homeassistant.components.synology_dsm.async_setup_entry", return_value=True + ): + yield diff --git a/tests/components/synology_dsm/consts.py b/tests/components/synology_dsm/consts.py new file mode 100644 index 00000000000..3c305745aa7 --- /dev/null +++ b/tests/components/synology_dsm/consts.py @@ -0,0 +1,14 @@ +"""Constants for the Synology DSM component tests.""" + +HOST = "nas.meontheinternet.com" +SERIAL = "mySerial" +HOST_2 = "nas.worldwide.me" +SERIAL_2 = "mySerial2" +PORT = 1234 +USE_SSL = True +VERIFY_SSL = False +USERNAME = "Home_Assistant" +PASSWORD = "password" +DEVICE_TOKEN = "Dév!cè_T0k€ñ" + +MACS = ["00-11-32-XX-XX-59", "00-11-32-XX-XX-5A"] diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index f895ee7e7dc..59ed8eea657 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -10,7 +10,6 @@ from synology_dsm.exceptions import ( from homeassistant import data_entry_flow, setup from homeassistant.components import ssdp -from homeassistant.components.synology_dsm import _async_setup_services from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( CONF_VOLUMES, @@ -21,7 +20,6 @@ from homeassistant.components.synology_dsm.const import ( DEFAULT_USE_SSL, DEFAULT_VERIFY_SSL, DOMAIN, - SERVICES, ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( @@ -38,22 +36,23 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType +from .consts import ( + DEVICE_TOKEN, + HOST, + HOST_2, + MACS, + PASSWORD, + PORT, + SERIAL, + SERIAL_2, + USE_SSL, + USERNAME, + VERIFY_SSL, +) + from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry -HOST = "nas.meontheinternet.com" -SERIAL = "mySerial" -HOST_2 = "nas.worldwide.me" -SERIAL_2 = "mySerial2" -PORT = 1234 -USE_SSL = True -VERIFY_SSL = False -USERNAME = "Home_Assistant" -PASSWORD = "password" -DEVICE_TOKEN = "Dév!cè_T0k€ñ" - -MACS = ["00-11-32-XX-XX-59", "00-11-32-XX-XX-5A"] - @pytest.fixture(name="service") def mock_controller_service(): @@ -498,23 +497,3 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 - - -async def test_services_registered(hass: HomeAssistantType): - """Test if all services are registered.""" - with patch( - "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) - ) as async_register: - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_SSL: USE_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) - await _async_setup_services(hass) - assert async_register.call_count == len(SERVICES) diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py new file mode 100644 index 00000000000..b8be375b321 --- /dev/null +++ b/tests/components/synology_dsm/test_init.py @@ -0,0 +1,41 @@ +"""Tests for the Synology DSM component.""" +import pytest + +from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.helpers.typing import HomeAssistantType + +from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +@pytest.mark.no_bypass_setup +async def test_services_registered(hass: HomeAssistantType): + """Test if all services are registered.""" + with patch( + "homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True + ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS[0], + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + for service in SERVICES: + assert hass.services.has_service(DOMAIN, service) From 4c2bf1ddf5a124dbb67d3799e5ed655c77ef901f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 16 Nov 2020 11:49:23 +0100 Subject: [PATCH 084/430] Avoid creating battery sensor if Shelly device is external powered (#43243) --- homeassistant/components/shelly/sensor.py | 36 +++++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 5d29aae8d5e..81dc2ef1c11 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,4 +1,6 @@ """Sensor for Shelly.""" +import logging + from homeassistant.components import sensor from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -12,7 +14,8 @@ from homeassistant.const import ( VOLT, ) -from .const import SHAIR_MAX_WORK_HOURS +from . import ShellyDeviceWrapper, get_device_name +from .const import DATA_CONFIG_ENTRY, DOMAIN, REST, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -21,12 +24,17 @@ from .entity import ( async_setup_entry_attribute_entities, async_setup_entry_rest, ) -from .utils import temperature_unit +from .utils import async_remove_entity_by_domain, temperature_unit -SENSORS = { +_LOGGER = logging.getLogger(__name__) + +BATTERY_SENSOR = { ("device", "battery"): BlockAttributeDescription( name="Battery", unit=PERCENTAGE, device_class=sensor.DEVICE_CLASS_BATTERY ), +} + +SENSORS = { ("device", "deviceTemp"): BlockAttributeDescription( name="Device Temperature", unit=temperature_unit, @@ -175,6 +183,28 @@ REST_SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" + + wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ][REST] + + if ( + "external_power" in wrapper.device.settings + and wrapper.device.settings["external_power"] == 1 + ): + _LOGGER.debug( + "Removed battery sensor [externally powered] for %s", + get_device_name(wrapper.device), + ) + unique_id = f'{wrapper.device.shelly["mac"]}-battery' + await async_remove_entity_by_domain( + hass, "sensor", unique_id, config_entry.entry_id + ) + else: + await async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, BATTERY_SENSOR, ShellySensor + ) + await async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, SENSORS, ShellySensor ) From 246ad8dba9d1b3bda8773a13512c552803c97c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Suwa=C5=82a?= <63507293+adriansuwala@users.noreply.github.com> Date: Mon, 16 Nov 2020 11:54:51 +0100 Subject: [PATCH 085/430] Rewrite ecobee unittest tests to pytest (#42584) --- tests/components/ecobee/test_climate.py | 522 ++++++++++---------- tests/components/ecobee/test_config_flow.py | 28 +- tests/components/ecobee/test_util.py | 8 +- 3 files changed, 292 insertions(+), 266 deletions(-) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 4e88d7083c4..32575e7188a 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -1,288 +1,310 @@ """The test for the Ecobee thermostat module.""" -import unittest from unittest import mock +import pytest + from homeassistant.components.ecobee import climate as ecobee import homeassistant.const as const from homeassistant.const import STATE_OFF -class TestEcobee(unittest.TestCase): - """Tests for Ecobee climate.""" - - def setUp(self): - """Set up test variables.""" - vals = { - "name": "Ecobee", - "program": { - "climates": [ - {"name": "Climate1", "climateRef": "c1"}, - {"name": "Climate2", "climateRef": "c2"}, - ], - "currentClimateRef": "c1", - }, - "runtime": { - "actualTemperature": 300, - "actualHumidity": 15, - "desiredHeat": 400, - "desiredCool": 200, - "desiredFanMode": "on", - }, - "settings": { - "hvacMode": "auto", - "heatStages": 1, - "coolStages": 1, - "fanMinOnTime": 10, - "heatCoolMinDelta": 50, - "holdAction": "nextTransition", - }, - "equipmentStatus": "fan", - "events": [ - { - "name": "Event1", - "running": True, - "type": "hold", - "holdClimateRef": "away", - "endDate": "2017-01-01 10:00:00", - "startDate": "2017-02-02 11:00:00", - } +@pytest.fixture +def ecobee_fixture(): + """Set up ecobee mock.""" + vals = { + "name": "Ecobee", + "program": { + "climates": [ + {"name": "Climate1", "climateRef": "c1"}, + {"name": "Climate2", "climateRef": "c2"}, ], - } + "currentClimateRef": "c1", + }, + "runtime": { + "actualTemperature": 300, + "actualHumidity": 15, + "desiredHeat": 400, + "desiredCool": 200, + "desiredFanMode": "on", + }, + "settings": { + "hvacMode": "auto", + "heatStages": 1, + "coolStages": 1, + "fanMinOnTime": 10, + "heatCoolMinDelta": 50, + "holdAction": "nextTransition", + }, + "equipmentStatus": "fan", + "events": [ + { + "name": "Event1", + "running": True, + "type": "hold", + "holdClimateRef": "away", + "endDate": "2017-01-01 10:00:00", + "startDate": "2017-02-02 11:00:00", + } + ], + } + mock_ecobee = mock.Mock() + mock_ecobee.__getitem__ = mock.Mock(side_effect=vals.__getitem__) + mock_ecobee.__setitem__ = mock.Mock(side_effect=vals.__setitem__) + return mock_ecobee - self.ecobee = mock.Mock() - self.ecobee.__getitem__ = mock.Mock(side_effect=vals.__getitem__) - self.ecobee.__setitem__ = mock.Mock(side_effect=vals.__setitem__) - self.data = mock.Mock() - self.data.ecobee.get_thermostat.return_value = self.ecobee - self.thermostat = ecobee.Thermostat(self.data, 1) +@pytest.fixture(name="data") +def data_fixture(ecobee_fixture): + """Set up data mock.""" + data = mock.Mock() + data.ecobee.get_thermostat.return_value = ecobee_fixture + return data - def test_name(self): - """Test name property.""" - assert "Ecobee" == self.thermostat.name - def test_current_temperature(self): - """Test current temperature.""" - assert 30 == self.thermostat.current_temperature - self.ecobee["runtime"]["actualTemperature"] = const.HTTP_NOT_FOUND - assert 40.4 == self.thermostat.current_temperature +@pytest.fixture(name="thermostat") +def thermostat_fixture(data): + """Set up ecobee thermostat object.""" + return ecobee.Thermostat(data, 1) - def test_target_temperature_low(self): - """Test target low temperature.""" - assert 40 == self.thermostat.target_temperature_low - self.ecobee["runtime"]["desiredHeat"] = 502 - assert 50.2 == self.thermostat.target_temperature_low - def test_target_temperature_high(self): - """Test target high temperature.""" - assert 20 == self.thermostat.target_temperature_high - self.ecobee["runtime"]["desiredCool"] = 103 - assert 10.3 == self.thermostat.target_temperature_high +async def test_name(thermostat): + """Test name property.""" + assert thermostat.name == "Ecobee" - def test_target_temperature(self): - """Test target temperature.""" - assert self.thermostat.target_temperature is None - self.ecobee["settings"]["hvacMode"] = "heat" - assert 40 == self.thermostat.target_temperature - self.ecobee["settings"]["hvacMode"] = "cool" - assert 20 == self.thermostat.target_temperature - self.ecobee["settings"]["hvacMode"] = "auxHeatOnly" - assert 40 == self.thermostat.target_temperature - self.ecobee["settings"]["hvacMode"] = "off" - assert self.thermostat.target_temperature is None - def test_desired_fan_mode(self): - """Test desired fan mode property.""" - assert "on" == self.thermostat.fan_mode - self.ecobee["runtime"]["desiredFanMode"] = "auto" - assert "auto" == self.thermostat.fan_mode +async def test_current_temperature(ecobee_fixture, thermostat): + """Test current temperature.""" + assert thermostat.current_temperature == 30 + ecobee_fixture["runtime"]["actualTemperature"] = const.HTTP_NOT_FOUND + assert thermostat.current_temperature == 40.4 - def test_fan(self): - """Test fan property.""" - assert const.STATE_ON == self.thermostat.fan - self.ecobee["equipmentStatus"] = "" - assert STATE_OFF == self.thermostat.fan - self.ecobee["equipmentStatus"] = "heatPump, heatPump2" - assert STATE_OFF == self.thermostat.fan - def test_hvac_mode(self): - """Test current operation property.""" - assert self.thermostat.hvac_mode == "heat_cool" - self.ecobee["settings"]["hvacMode"] = "heat" - assert self.thermostat.hvac_mode == "heat" - self.ecobee["settings"]["hvacMode"] = "cool" - assert self.thermostat.hvac_mode == "cool" - self.ecobee["settings"]["hvacMode"] = "auxHeatOnly" - assert self.thermostat.hvac_mode == "heat" - self.ecobee["settings"]["hvacMode"] = "off" - assert self.thermostat.hvac_mode == "off" +async def test_target_temperature_low(ecobee_fixture, thermostat): + """Test target low temperature.""" + assert thermostat.target_temperature_low == 40 + ecobee_fixture["runtime"]["desiredHeat"] = 502 + assert thermostat.target_temperature_low == 50.2 - def test_hvac_modes(self): - """Test operation list property.""" - assert ["heat_cool", "heat", "cool", "off"] == self.thermostat.hvac_modes - def test_hvac_mode2(self): - """Test operation mode property.""" - assert self.thermostat.hvac_mode == "heat_cool" - self.ecobee["settings"]["hvacMode"] = "heat" - assert self.thermostat.hvac_mode == "heat" +async def test_target_temperature_high(ecobee_fixture, thermostat): + """Test target high temperature.""" + assert thermostat.target_temperature_high == 20 + ecobee_fixture["runtime"]["desiredCool"] = 103 + assert thermostat.target_temperature_high == 10.3 - def test_device_state_attributes(self): - """Test device state attributes property.""" - self.ecobee["equipmentStatus"] = "heatPump2" - assert { - "fan": "off", - "climate_mode": "Climate1", - "fan_min_on_time": 10, - "equipment_running": "heatPump2", - } == self.thermostat.device_state_attributes - self.ecobee["equipmentStatus"] = "auxHeat2" - assert { - "fan": "off", - "climate_mode": "Climate1", - "fan_min_on_time": 10, - "equipment_running": "auxHeat2", - } == self.thermostat.device_state_attributes - self.ecobee["equipmentStatus"] = "compCool1" - assert { - "fan": "off", - "climate_mode": "Climate1", - "fan_min_on_time": 10, - "equipment_running": "compCool1", - } == self.thermostat.device_state_attributes - self.ecobee["equipmentStatus"] = "" - assert { - "fan": "off", - "climate_mode": "Climate1", - "fan_min_on_time": 10, - "equipment_running": "", - } == self.thermostat.device_state_attributes +async def test_target_temperature(ecobee_fixture, thermostat): + """Test target temperature.""" + assert thermostat.target_temperature is None + ecobee_fixture["settings"]["hvacMode"] = "heat" + assert thermostat.target_temperature == 40 + ecobee_fixture["settings"]["hvacMode"] = "cool" + assert thermostat.target_temperature == 20 + ecobee_fixture["settings"]["hvacMode"] = "auxHeatOnly" + assert thermostat.target_temperature == 40 + ecobee_fixture["settings"]["hvacMode"] = "off" + assert thermostat.target_temperature is None - self.ecobee["equipmentStatus"] = "Unknown" - assert { - "fan": "off", - "climate_mode": "Climate1", - "fan_min_on_time": 10, - "equipment_running": "Unknown", - } == self.thermostat.device_state_attributes - self.ecobee["program"]["currentClimateRef"] = "c2" - assert { - "fan": "off", - "climate_mode": "Climate2", - "fan_min_on_time": 10, - "equipment_running": "Unknown", - } == self.thermostat.device_state_attributes +async def test_desired_fan_mode(ecobee_fixture, thermostat): + """Test desired fan mode property.""" + assert thermostat.fan_mode == "on" + ecobee_fixture["runtime"]["desiredFanMode"] = "auto" + assert thermostat.fan_mode == "auto" - def test_is_aux_heat_on(self): - """Test aux heat property.""" - assert not self.thermostat.is_aux_heat - self.ecobee["equipmentStatus"] = "fan, auxHeat" - assert self.thermostat.is_aux_heat - def test_set_temperature(self): - """Test set temperature.""" - # Auto -> Auto - self.data.reset_mock() - self.thermostat.set_temperature(target_temp_low=20, target_temp_high=30) - self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 20, "nextTransition")] - ) +async def test_fan(ecobee_fixture, thermostat): + """Test fan property.""" + assert const.STATE_ON == thermostat.fan + ecobee_fixture["equipmentStatus"] = "" + assert STATE_OFF == thermostat.fan + ecobee_fixture["equipmentStatus"] = "heatPump, heatPump2" + assert STATE_OFF == thermostat.fan - # Auto -> Hold - self.data.reset_mock() - self.thermostat.set_temperature(temperature=20) - self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 25, 15, "nextTransition")] - ) - # Cool -> Hold - self.data.reset_mock() - self.ecobee["settings"]["hvacMode"] = "cool" - self.thermostat.set_temperature(temperature=20.5) - self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20.5, 20.5, "nextTransition")] - ) +async def test_hvac_mode(ecobee_fixture, thermostat): + """Test current operation property.""" + assert thermostat.hvac_mode == "heat_cool" + ecobee_fixture["settings"]["hvacMode"] = "heat" + assert thermostat.hvac_mode == "heat" + ecobee_fixture["settings"]["hvacMode"] = "cool" + assert thermostat.hvac_mode == "cool" + ecobee_fixture["settings"]["hvacMode"] = "auxHeatOnly" + assert thermostat.hvac_mode == "heat" + ecobee_fixture["settings"]["hvacMode"] = "off" + assert thermostat.hvac_mode == "off" - # Heat -> Hold - self.data.reset_mock() - self.ecobee["settings"]["hvacMode"] = "heat" - self.thermostat.set_temperature(temperature=20) - self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20, 20, "nextTransition")] - ) - # Heat -> Auto - self.data.reset_mock() - self.ecobee["settings"]["hvacMode"] = "heat" - self.thermostat.set_temperature(target_temp_low=20, target_temp_high=30) - assert not self.data.ecobee.set_hold_temp.called +async def test_hvac_modes(thermostat): + """Test operation list property.""" + assert ["heat_cool", "heat", "cool", "off"] == thermostat.hvac_modes - def test_set_hvac_mode(self): - """Test operation mode setter.""" - self.data.reset_mock() - self.thermostat.set_hvac_mode("heat_cool") - self.data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "auto")]) - self.data.reset_mock() - self.thermostat.set_hvac_mode("heat") - self.data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "heat")]) - def test_set_fan_min_on_time(self): - """Test fan min on time setter.""" - self.data.reset_mock() - self.thermostat.set_fan_min_on_time(15) - self.data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 15)]) - self.data.reset_mock() - self.thermostat.set_fan_min_on_time(20) - self.data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 20)]) +async def test_hvac_mode2(ecobee_fixture, thermostat): + """Test operation mode property.""" + assert thermostat.hvac_mode == "heat_cool" + ecobee_fixture["settings"]["hvacMode"] = "heat" + assert thermostat.hvac_mode == "heat" - def test_resume_program(self): - """Test resume program.""" - # False - self.data.reset_mock() - self.thermostat.resume_program(False) - self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")]) - self.data.reset_mock() - self.thermostat.resume_program(None) - self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")]) - self.data.reset_mock() - self.thermostat.resume_program(0) - self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")]) - # True - self.data.reset_mock() - self.thermostat.resume_program(True) - self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")]) - self.data.reset_mock() - self.thermostat.resume_program(1) - self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")]) +async def test_device_state_attributes(ecobee_fixture, thermostat): + """Test device state attributes property.""" + ecobee_fixture["equipmentStatus"] = "heatPump2" + assert { + "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "heatPump2", + } == thermostat.device_state_attributes - def test_hold_preference(self): - """Test hold preference.""" - assert "nextTransition" == self.thermostat.hold_preference() - for action in [ - "useEndTime4hour", - "useEndTime2hour", - "nextPeriod", - "indefinite", - "askMe", - ]: - self.ecobee["settings"]["holdAction"] = action - assert "nextTransition" == self.thermostat.hold_preference() + ecobee_fixture["equipmentStatus"] = "auxHeat2" + assert { + "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "auxHeat2", + } == thermostat.device_state_attributes + ecobee_fixture["equipmentStatus"] = "compCool1" + assert { + "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "compCool1", + } == thermostat.device_state_attributes + ecobee_fixture["equipmentStatus"] = "" + assert { + "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "", + } == thermostat.device_state_attributes - def test_set_fan_mode_on(self): - """Test set fan mode to on.""" - self.data.reset_mock() - self.thermostat.set_fan_mode("on") - self.data.ecobee.set_fan_mode.assert_has_calls( - [mock.call(1, "on", 20, 40, "nextTransition")] - ) + ecobee_fixture["equipmentStatus"] = "Unknown" + assert { + "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "Unknown", + } == thermostat.device_state_attributes - def test_set_fan_mode_auto(self): - """Test set fan mode to auto.""" - self.data.reset_mock() - self.thermostat.set_fan_mode("auto") - self.data.ecobee.set_fan_mode.assert_has_calls( - [mock.call(1, "auto", 20, 40, "nextTransition")] - ) + ecobee_fixture["program"]["currentClimateRef"] = "c2" + assert { + "fan": "off", + "climate_mode": "Climate2", + "fan_min_on_time": 10, + "equipment_running": "Unknown", + } == thermostat.device_state_attributes + + +async def test_is_aux_heat_on(ecobee_fixture, thermostat): + """Test aux heat property.""" + assert not thermostat.is_aux_heat + ecobee_fixture["equipmentStatus"] = "fan, auxHeat" + assert thermostat.is_aux_heat + + +async def test_set_temperature(ecobee_fixture, thermostat, data): + """Test set temperature.""" + # Auto -> Auto + data.reset_mock() + thermostat.set_temperature(target_temp_low=20, target_temp_high=30) + data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 30, 20, "nextTransition")]) + + # Auto -> Hold + data.reset_mock() + thermostat.set_temperature(temperature=20) + data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 25, 15, "nextTransition")]) + + # Cool -> Hold + data.reset_mock() + ecobee_fixture["settings"]["hvacMode"] = "cool" + thermostat.set_temperature(temperature=20.5) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 20.5, 20.5, "nextTransition")] + ) + + # Heat -> Hold + data.reset_mock() + ecobee_fixture["settings"]["hvacMode"] = "heat" + thermostat.set_temperature(temperature=20) + data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 20, 20, "nextTransition")]) + + # Heat -> Auto + data.reset_mock() + ecobee_fixture["settings"]["hvacMode"] = "heat" + thermostat.set_temperature(target_temp_low=20, target_temp_high=30) + assert not data.ecobee.set_hold_temp.called + + +async def test_set_hvac_mode(thermostat, data): + """Test operation mode setter.""" + data.reset_mock() + thermostat.set_hvac_mode("heat_cool") + data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "auto")]) + data.reset_mock() + thermostat.set_hvac_mode("heat") + data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "heat")]) + + +async def test_set_fan_min_on_time(thermostat, data): + """Test fan min on time setter.""" + data.reset_mock() + thermostat.set_fan_min_on_time(15) + data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 15)]) + data.reset_mock() + thermostat.set_fan_min_on_time(20) + data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 20)]) + + +async def test_resume_program(thermostat, data): + """Test resume program.""" + # False + data.reset_mock() + thermostat.resume_program(False) + data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")]) + data.reset_mock() + thermostat.resume_program(None) + data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")]) + data.reset_mock() + thermostat.resume_program(0) + data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")]) + + # True + data.reset_mock() + thermostat.resume_program(True) + data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")]) + data.reset_mock() + thermostat.resume_program(1) + data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")]) + + +async def test_hold_preference(ecobee_fixture, thermostat): + """Test hold preference.""" + assert thermostat.hold_preference() == "nextTransition" + for action in [ + "useEndTime4hour", + "useEndTime2hour", + "nextPeriod", + "indefinite", + "askMe", + ]: + ecobee_fixture["settings"]["holdAction"] = action + assert thermostat.hold_preference() == "nextTransition" + + +async def test_set_fan_mode_on(thermostat, data): + """Test set fan mode to on.""" + data.reset_mock() + thermostat.set_fan_mode("on") + data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, "on", 20, 40, "nextTransition")] + ) + + +async def test_set_fan_mode_auto(thermostat, data): + """Test set fan mode to auto.""" + data.reset_mock() + thermostat.set_fan_mode("auto") + data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, "auto", 20, 40, "nextTransition")] + ) diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 88b7eb7241d..8311b4aba2c 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -46,8 +46,8 @@ async def test_pin_request_succeeds(hass): flow.hass = hass flow.hass.data[DATA_ECOBEE_CONFIG] = {} - with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: - mock_ecobee = MockEcobee.return_value + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee: + mock_ecobee = mock_ecobee.return_value mock_ecobee.request_pin.return_value = True mock_ecobee.pin = "test-pin" @@ -64,8 +64,8 @@ async def test_pin_request_fails(hass): flow.hass = hass flow.hass.data[DATA_ECOBEE_CONFIG] = {} - with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: - mock_ecobee = MockEcobee.return_value + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee: + mock_ecobee = mock_ecobee.return_value mock_ecobee.request_pin.return_value = False result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) @@ -81,12 +81,14 @@ async def test_token_request_succeeds(hass): flow.hass = hass flow.hass.data[DATA_ECOBEE_CONFIG] = {} - with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: - mock_ecobee = MockEcobee.return_value + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee: + mock_ecobee = mock_ecobee.return_value mock_ecobee.request_tokens.return_value = True mock_ecobee.api_key = "test-api-key" mock_ecobee.refresh_token = "test-token" + # pylint: disable=protected-access flow._ecobee = mock_ecobee + # pylint: enable=protected-access result = await flow.async_step_authorize(user_input={}) @@ -104,11 +106,13 @@ async def test_token_request_fails(hass): flow.hass = hass flow.hass.data[DATA_ECOBEE_CONFIG] = {} - with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: - mock_ecobee = MockEcobee.return_value + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee: + mock_ecobee = mock_ecobee.return_value mock_ecobee.request_tokens.return_value = False mock_ecobee.pin = "test-pin" + # pylint: disable=protected-access flow._ecobee = mock_ecobee + # pylint: enable=protected-access result = await flow.async_step_authorize(user_input={}) @@ -143,8 +147,8 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t 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 + ), patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee: + mock_ecobee = mock_ecobee.return_value mock_ecobee.refresh_tokens.return_value = True mock_ecobee.api_key = "test-api-key" mock_ecobee.refresh_token = "test-token" @@ -196,10 +200,10 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_t return_value=MOCK_ECOBEE_CONF, ), patch( "homeassistant.components.ecobee.config_flow.Ecobee" - ) as MockEcobee, patch.object( + ) as mock_ecobee, patch.object( flow, "async_step_user", return_value=mock_coro() ) as mock_async_step_user: - mock_ecobee = MockEcobee.return_value + mock_ecobee = mock_ecobee.return_value mock_ecobee.refresh_tokens.return_value = False await flow.async_step_import(import_data=None) diff --git a/tests/components/ecobee/test_util.py b/tests/components/ecobee/test_util.py index ee02f2a33aa..d159fd697d5 100644 --- a/tests/components/ecobee/test_util.py +++ b/tests/components/ecobee/test_util.py @@ -5,14 +5,14 @@ import voluptuous as vol from homeassistant.components.ecobee.util import ecobee_date, ecobee_time -def test_ecobee_date_with_valid_input(): +async 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(): +async def test_ecobee_date_with_invalid_input(): """Test that the date function raises the expected exception.""" test_input = "20190927" @@ -20,14 +20,14 @@ def test_ecobee_date_with_invalid_input(): ecobee_date(test_input) -def test_ecobee_time_with_valid_input(): +async 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(): +async def test_ecobee_time_with_invalid_input(): """Test that the time function raises the expected exception.""" test_input = "20:55" From 1783e1ae64aa2e9411135555ba7998af316a0198 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 16 Nov 2020 12:22:32 +0100 Subject: [PATCH 086/430] Bump python-miio and construct version (#43267) --- homeassistant/components/eddystone_temperature/manifest.json | 2 +- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index c59cb6a9c7f..e6ff0a17ea3 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -2,6 +2,6 @@ "domain": "eddystone_temperature", "name": "Eddystone", "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", - "requirements": ["beacontools[scan]==1.2.3", "construct==2.9.45"], + "requirements": ["beacontools[scan]==1.2.3", "construct==2.10.56"], "codeowners": [] } diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index e15fd8d384b..5f5fefe25ea 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -2,6 +2,6 @@ "domain": "eq3btsmart", "name": "EQ3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", - "requirements": ["construct==2.9.45", "python-eq3bt==0.1.11"], + "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], "codeowners": ["@rytilahti"] } diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 853f8e7920b..2536b0e0aa7 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.9.45", "python-miio==0.5.3"], + "requirements": ["construct==2.10.56", "python-miio==0.5.4"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 56027b97ae6..a8089e8af4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -448,7 +448,7 @@ connect-box==0.2.8 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart # homeassistant.components.xiaomi_miio -construct==2.9.45 +construct==2.10.56 # homeassistant.components.coronavirus coronavirus==1.1.1 @@ -1773,7 +1773,7 @@ python-juicenet==1.0.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.3 +python-miio==0.5.4 # homeassistant.components.mpd python-mpd2==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0df8aa39764..37d432eaf17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ colorthief==0.2.1 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart # homeassistant.components.xiaomi_miio -construct==2.9.45 +construct==2.10.56 # homeassistant.components.coronavirus coronavirus==1.1.1 @@ -869,7 +869,7 @@ python-izone==1.1.2 python-juicenet==1.0.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.3 +python-miio==0.5.4 # homeassistant.components.nest python-nest==4.1.0 From 7280dbd431aef1bba1647bff7a83d5a04779cffc Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 16 Nov 2020 12:27:42 +0100 Subject: [PATCH 087/430] Bump requests to 2.25.0 (#43279) --- homeassistant/package_constraints.txt | 2 +- requirements.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 0fb7dcd3274..f80811b668f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ pip>=8.0.3 python-slugify==4.0.1 pytz>=2020.1 pyyaml==5.3.1 -requests==2.24.0 +requests==2.25.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 diff --git a/requirements.txt b/requirements.txt index 080a41a884b..3a376b6e7cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ pip>=8.0.3 python-slugify==4.0.1 pytz>=2020.1 pyyaml==5.3.1 -requests==2.24.0 +requests==2.25.0 ruamel.yaml==0.15.100 voluptuous==0.12.0 voluptuous-serialize==2.4.0 diff --git a/setup.py b/setup.py index bb5d06b6acd..885d6e192d6 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ "python-slugify==4.0.1", "pytz>=2020.1", "pyyaml==5.3.1", - "requests==2.24.0", + "requests==2.25.0", "ruamel.yaml==0.15.100", "voluptuous==0.12.0", "voluptuous-serialize==2.4.0", From 5ea55f7e8941f539f45cfd6260b25479c15134ad Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky <3136012+GuyKh@users.noreply.github.com> Date: Mon, 16 Nov 2020 13:31:45 +0200 Subject: [PATCH 088/430] Xiaomi Device Tracker - Move "Refreshing device list" to debug (#43276) --- homeassistant/components/xiaomi/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 530140b524f..4fe485b193b 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -85,12 +85,12 @@ class XiaomiDeviceScanner(DeviceScanner): Return the list if successful. """ - _LOGGER.info("Refreshing device list") + _LOGGER.debug("Refreshing device list") result = _retrieve_list(self.host, self.token) if result: return result - _LOGGER.info("Refreshing token and retrying device list refresh") + _LOGGER.debug("Refreshing token and retrying device list refresh") self.token = _get_token(self.host, self.username, self.password) return _retrieve_list(self.host, self.token) From 63abe8b4f9fb0ac21f851e1b017654e2e4c88924 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 12:39:14 +0100 Subject: [PATCH 089/430] Bump actions/upload-artifact from v2.2.0 to v2.2.1 (#43272) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from v2.2.0 to v2.2.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2.2.0...726a6dcd0199f578459862705eed35cda05af50b) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ad6becee4e2..48e4244600b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -741,7 +741,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.0 + uses: actions/upload-artifact@v2.2.1 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage From 31f3ce81eee1121548f891df7fe545334d1fe36e Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 16 Nov 2020 12:16:49 +0000 Subject: [PATCH 090/430] Bump pyvera to 0.3.11 (#43262) --- homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index b41d289e6b3..264f44782f5 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,6 +3,6 @@ "name": "Vera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", - "requirements": ["pyvera==0.3.10"], + "requirements": ["pyvera==0.3.11"], "codeowners": ["@vangorra"] } diff --git a/requirements_all.txt b/requirements_all.txt index a8089e8af4f..db15fefdd1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1867,7 +1867,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.10 +pyvera==0.3.11 # homeassistant.components.versasense pyversasense==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37d432eaf17..cb7dbbfe757 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -905,7 +905,7 @@ pytraccar==0.9.0 pytradfri[async]==7.0.2 # homeassistant.components.vera -pyvera==0.3.10 +pyvera==0.3.11 # homeassistant.components.vesync pyvesync==1.2.0 From 5d83f0a9116be160e885fbba20bbeef02a0849b4 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 16 Nov 2020 12:17:58 +0000 Subject: [PATCH 091/430] Bump Pywemo to 0.5.3 (#43263) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index a7d2ca585a5..e986913fc70 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.5.2"], + "requirements": ["pywemo==0.5.3"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index db15fefdd1b..95d49a43fb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1888,7 +1888,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.2 +pywemo==0.5.3 # homeassistant.components.wilight pywilight==0.0.65 From 819dd27925c0cbcb45c85ce81c32746538d6a6fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Nov 2020 15:43:48 +0100 Subject: [PATCH 092/430] Automatically clean up executor as part of closing loop (#43284) --- homeassistant/bootstrap.py | 10 +- homeassistant/core.py | 14 +-- homeassistant/runner.py | 28 +---- tests/common.py | 14 +-- tests/test_core.py | 220 ++++++++++++++++++------------------- 5 files changed, 120 insertions(+), 166 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0d63307a020..eff8a04ba92 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -15,11 +15,7 @@ import yarl from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, - REQUIRED_NEXT_PYTHON_DATE, - REQUIRED_NEXT_PYTHON_VER, -) +from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ( @@ -142,11 +138,9 @@ async def async_setup_hass( _LOGGER.warning("Detected that frontend did not load. Activating safe mode") # Ask integrations to shut down. It's messy but we can't # do a clean stop without knowing what is broken - hass.async_track_tasks() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {}) with contextlib.suppress(asyncio.TimeoutError): async with hass.timeout.async_timeout(10): - await hass.async_block_till_done() + await hass.async_stop() safe_mode = True old_config = hass.config diff --git a/homeassistant/core.py b/homeassistant/core.py index ed8ae854106..68f0b9a30b7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -257,12 +257,9 @@ class HomeAssistant: fire_coroutine_threadsafe(self.async_start(), self.loop) # Run forever - try: - # Block until stopped - _LOGGER.info("Starting Home Assistant core loop") - self.loop.run_forever() - finally: - self.loop.close() + # Block until stopped + _LOGGER.info("Starting Home Assistant core loop") + self.loop.run_forever() return self.exit_code async def async_run(self, *, attach_signals: bool = True) -> int: @@ -559,16 +556,11 @@ class HomeAssistant: "Timed out waiting for shutdown stage 3 to complete, the shutdown will continue" ) - # Python 3.9+ and backported in runner.py - await self.loop.shutdown_default_executor() # type: ignore - self.exit_code = exit_code self.state = CoreState.stopped if self._stopped is not None: self._stopped.set() - else: - self.loop.stop() @attr.s(slots=True, frozen=True) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index a5cf0f88a40..0f8bb836da5 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -4,7 +4,6 @@ from concurrent.futures import ThreadPoolExecutor import dataclasses import logging import sys -import threading from typing import Any, Dict, Optional from homeassistant import bootstrap @@ -77,29 +76,14 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore loop.set_default_executor, "sets default executor on the event loop" ) - # Python 3.9+ - if hasattr(loop, "shutdown_default_executor"): - return loop + # Shut down executor when we shut down loop + orig_close = loop.close - # Copied from Python 3.9 source - def _do_shutdown(future: asyncio.Future) -> None: - try: - executor.shutdown(wait=True) - loop.call_soon_threadsafe(future.set_result, None) - except Exception as ex: # pylint: disable=broad-except - loop.call_soon_threadsafe(future.set_exception, ex) + def close() -> None: + executor.shutdown(wait=True) + orig_close() - async def shutdown_default_executor() -> None: - """Schedule the shutdown of the default executor.""" - future = loop.create_future() - thread = threading.Thread(target=_do_shutdown, args=(future,)) - thread.start() - try: - await future - finally: - thread.join() - - setattr(loop, "shutdown_default_executor", shutdown_default_executor) + loop.close = close # type: ignore return loop diff --git a/tests/common.py b/tests/common.py index 611becabe33..f0994308fe6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -9,7 +9,6 @@ from io import StringIO import json import logging import os -import sys import threading import time import uuid @@ -109,24 +108,21 @@ def get_test_config_dir(*add_path): def get_test_home_assistant(): """Return a Home Assistant object pointing at test config directory.""" - if sys.platform == "win32": - loop = asyncio.ProactorEventLoop() - else: - loop = asyncio.new_event_loop() - + loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) hass = loop.run_until_complete(async_test_home_assistant(loop)) - stop_event = threading.Event() + loop_stop_event = threading.Event() def run_loop(): """Run event loop.""" # pylint: disable=protected-access loop._thread_ident = threading.get_ident() loop.run_forever() - stop_event.set() + loop_stop_event.set() orig_stop = hass.stop + hass._stopped = Mock(set=loop.stop) def start_hass(*mocks): """Start hass.""" @@ -135,7 +131,7 @@ def get_test_home_assistant(): def stop_hass(): """Stop hass.""" orig_stop() - stop_event.wait() + loop_stop_event.wait() loop.close() hass.start = start_hass diff --git a/tests/test_core.py b/tests/test_core.py index f08de049efa..5ebfc070ca2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -38,7 +38,11 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from tests.async_mock import MagicMock, Mock, PropertyMock, patch -from tests.common import async_mock_service, get_test_home_assistant +from tests.common import ( + async_capture_events, + async_mock_service, + get_test_home_assistant, +) PST = pytz.timezone("America/Los_Angeles") @@ -151,22 +155,14 @@ def test_async_run_hass_job_delegates_non_async(): assert len(hass.async_add_hass_job.mock_calls) == 1 -def test_stage_shutdown(): +async def test_stage_shutdown(hass): """Simulate a shutdown, test calling stuff.""" - hass = get_test_home_assistant() - test_stop = [] - test_final_write = [] - test_close = [] - test_all = [] + test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) + test_final_write = async_capture_events(hass, EVENT_HOMEASSISTANT_FINAL_WRITE) + test_close = async_capture_events(hass, EVENT_HOMEASSISTANT_CLOSE) + test_all = async_capture_events(hass, MATCH_ALL) - hass.bus.listen(EVENT_HOMEASSISTANT_STOP, lambda event: test_stop.append(event)) - hass.bus.listen( - EVENT_HOMEASSISTANT_FINAL_WRITE, lambda event: test_final_write.append(event) - ) - hass.bus.listen(EVENT_HOMEASSISTANT_CLOSE, lambda event: test_close.append(event)) - hass.bus.listen("*", lambda event: test_all.append(event)) - - hass.stop() + await hass.async_stop() assert len(test_stop) == 1 assert len(test_close) == 1 @@ -341,147 +337,139 @@ def test_state_as_dict(): assert state.as_dict() is state.as_dict() -class TestEventBus(unittest.TestCase): - """Test EventBus methods.""" +async def test_add_remove_listener(hass): + """Test remove_listener method.""" + old_count = len(hass.bus.async_listeners()) - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.bus = self.hass.bus + def listener(_): + pass - # pylint: disable=invalid-name - def tearDown(self): - """Stop down stuff we started.""" - self.hass.stop() + unsub = hass.bus.async_listen("test", listener) - def test_add_remove_listener(self): - """Test remove_listener method.""" - self.hass.allow_pool = False - old_count = len(self.bus.listeners) + assert old_count + 1 == len(hass.bus.async_listeners()) - def listener(_): - pass + # Remove listener + unsub() + assert old_count == len(hass.bus.async_listeners()) - unsub = self.bus.listen("test", listener) + # Should do nothing now + unsub() - assert old_count + 1 == len(self.bus.listeners) - # Remove listener - unsub() - assert old_count == len(self.bus.listeners) +async def test_unsubscribe_listener(hass): + """Test unsubscribe listener from returned function.""" + calls = [] - # Should do nothing now - unsub() + @ha.callback + def listener(event): + """Mock listener.""" + calls.append(event) - def test_unsubscribe_listener(self): - """Test unsubscribe listener from returned function.""" - calls = [] + unsub = hass.bus.async_listen("test", listener) - @ha.callback - def listener(event): - """Mock listener.""" - calls.append(event) + hass.bus.async_fire("test") + await hass.async_block_till_done() - unsub = self.bus.listen("test", listener) + assert len(calls) == 1 - self.bus.fire("test") - self.hass.block_till_done() + unsub() - assert len(calls) == 1 + hass.bus.async_fire("event") + await hass.async_block_till_done() - unsub() + assert len(calls) == 1 - self.bus.fire("event") - self.hass.block_till_done() - assert len(calls) == 1 +async def test_listen_once_event_with_callback(hass): + """Test listen_once_event method.""" + runs = [] - def test_listen_once_event_with_callback(self): - """Test listen_once_event method.""" - runs = [] + @ha.callback + def event_handler(event): + runs.append(event) - @ha.callback - def event_handler(event): - runs.append(event) + hass.bus.async_listen_once("test_event", event_handler) - self.bus.listen_once("test_event", event_handler) + hass.bus.async_fire("test_event") + # Second time it should not increase runs + hass.bus.async_fire("test_event") - self.bus.fire("test_event") - # Second time it should not increase runs - self.bus.fire("test_event") + await hass.async_block_till_done() + assert len(runs) == 1 - self.hass.block_till_done() - assert len(runs) == 1 - def test_listen_once_event_with_coroutine(self): - """Test listen_once_event method.""" - runs = [] +async def test_listen_once_event_with_coroutine(hass): + """Test listen_once_event method.""" + runs = [] - async def event_handler(event): - runs.append(event) + async def event_handler(event): + runs.append(event) - self.bus.listen_once("test_event", event_handler) + hass.bus.async_listen_once("test_event", event_handler) - self.bus.fire("test_event") - # Second time it should not increase runs - self.bus.fire("test_event") + hass.bus.async_fire("test_event") + # Second time it should not increase runs + hass.bus.async_fire("test_event") - self.hass.block_till_done() - assert len(runs) == 1 + await hass.async_block_till_done() + assert len(runs) == 1 - def test_listen_once_event_with_thread(self): - """Test listen_once_event method.""" - runs = [] - def event_handler(event): - runs.append(event) +async def test_listen_once_event_with_thread(hass): + """Test listen_once_event method.""" + runs = [] - self.bus.listen_once("test_event", event_handler) + def event_handler(event): + runs.append(event) - self.bus.fire("test_event") - # Second time it should not increase runs - self.bus.fire("test_event") + hass.bus.async_listen_once("test_event", event_handler) - self.hass.block_till_done() - assert len(runs) == 1 + hass.bus.async_fire("test_event") + # Second time it should not increase runs + hass.bus.async_fire("test_event") - def test_thread_event_listener(self): - """Test thread event listener.""" - thread_calls = [] + await hass.async_block_till_done() + assert len(runs) == 1 - def thread_listener(event): - thread_calls.append(event) - self.bus.listen("test_thread", thread_listener) - self.bus.fire("test_thread") - self.hass.block_till_done() - assert len(thread_calls) == 1 +async def test_thread_event_listener(hass): + """Test thread event listener.""" + thread_calls = [] - def test_callback_event_listener(self): - """Test callback event listener.""" - callback_calls = [] + def thread_listener(event): + thread_calls.append(event) - @ha.callback - def callback_listener(event): - callback_calls.append(event) + hass.bus.async_listen("test_thread", thread_listener) + hass.bus.async_fire("test_thread") + await hass.async_block_till_done() + assert len(thread_calls) == 1 - self.bus.listen("test_callback", callback_listener) - self.bus.fire("test_callback") - self.hass.block_till_done() - assert len(callback_calls) == 1 - def test_coroutine_event_listener(self): - """Test coroutine event listener.""" - coroutine_calls = [] +async def test_callback_event_listener(hass): + """Test callback event listener.""" + callback_calls = [] - async def coroutine_listener(event): - coroutine_calls.append(event) + @ha.callback + def callback_listener(event): + callback_calls.append(event) - self.bus.listen("test_coroutine", coroutine_listener) - self.bus.fire("test_coroutine") - self.hass.block_till_done() - assert len(coroutine_calls) == 1 + hass.bus.async_listen("test_callback", callback_listener) + hass.bus.async_fire("test_callback") + await hass.async_block_till_done() + assert len(callback_calls) == 1 + + +async def test_coroutine_event_listener(hass): + """Test coroutine event listener.""" + coroutine_calls = [] + + async def coroutine_listener(event): + coroutine_calls.append(event) + + hass.bus.async_listen("test_coroutine", coroutine_listener) + hass.bus.async_fire("test_coroutine") + await hass.async_block_till_done() + assert len(coroutine_calls) == 1 def test_state_init(): From 479de9433c677b8e8234d7841eb699b8b6ebe31c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Nov 2020 18:25:55 +0100 Subject: [PATCH 093/430] Convert core tests to async (#43287) --- tests/test_core.py | 464 +++++++++++++++++++-------------------------- 1 file changed, 195 insertions(+), 269 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 5ebfc070ca2..541f75e6343 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,7 +6,6 @@ import functools import logging import os from tempfile import TemporaryDirectory -import unittest import pytest import pytz @@ -33,16 +32,16 @@ from homeassistant.const import ( __version__, ) import homeassistant.core as ha -from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError +from homeassistant.exceptions import ( + InvalidEntityFormatError, + InvalidStateError, + ServiceNotFound, +) import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from tests.async_mock import MagicMock, Mock, PropertyMock, patch -from tests.common import ( - async_capture_events, - async_mock_service, - get_test_home_assistant, -) +from tests.common import async_capture_events, async_mock_service PST = pytz.timezone("America/Los_Angeles") @@ -337,7 +336,7 @@ def test_state_as_dict(): assert state.as_dict() is state.as_dict() -async def test_add_remove_listener(hass): +async def test_eventbus_add_remove_listener(hass): """Test remove_listener method.""" old_count = len(hass.bus.async_listeners()) @@ -356,7 +355,7 @@ async def test_add_remove_listener(hass): unsub() -async def test_unsubscribe_listener(hass): +async def test_eventbus_unsubscribe_listener(hass): """Test unsubscribe listener from returned function.""" calls = [] @@ -380,7 +379,7 @@ async def test_unsubscribe_listener(hass): assert len(calls) == 1 -async def test_listen_once_event_with_callback(hass): +async def test_eventbus_listen_once_event_with_callback(hass): """Test listen_once_event method.""" runs = [] @@ -398,7 +397,7 @@ async def test_listen_once_event_with_callback(hass): assert len(runs) == 1 -async def test_listen_once_event_with_coroutine(hass): +async def test_eventbus_listen_once_event_with_coroutine(hass): """Test listen_once_event method.""" runs = [] @@ -415,7 +414,7 @@ async def test_listen_once_event_with_coroutine(hass): assert len(runs) == 1 -async def test_listen_once_event_with_thread(hass): +async def test_eventbus_listen_once_event_with_thread(hass): """Test listen_once_event method.""" runs = [] @@ -432,7 +431,7 @@ async def test_listen_once_event_with_thread(hass): assert len(runs) == 1 -async def test_thread_event_listener(hass): +async def test_eventbus_thread_event_listener(hass): """Test thread event listener.""" thread_calls = [] @@ -445,7 +444,7 @@ async def test_thread_event_listener(hass): assert len(thread_calls) == 1 -async def test_callback_event_listener(hass): +async def test_eventbus_callback_event_listener(hass): """Test callback event listener.""" callback_calls = [] @@ -459,7 +458,7 @@ async def test_callback_event_listener(hass): assert len(callback_calls) == 1 -async def test_coroutine_event_listener(hass): +async def test_eventbus_coroutine_event_listener(hass): """Test coroutine event listener.""" coroutine_calls = [] @@ -550,117 +549,92 @@ def test_state_repr(): ) -class TestStateMachine(unittest.TestCase): - """Test State machine methods.""" +async def test_statemachine_is_state(hass): + """Test is_state method.""" + hass.states.async_set("light.bowl", "on", {}) + assert hass.states.is_state("light.Bowl", "on") + assert not hass.states.is_state("light.Bowl", "off") + assert not hass.states.is_state("light.Non_existing", "on") - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.states = self.hass.states - self.states.set("light.Bowl", "on") - self.states.set("switch.AC", "off") - # pylint: disable=invalid-name - def tearDown(self): - """Stop down stuff we started.""" - self.hass.stop() +async def test_statemachine_entity_ids(hass): + """Test get_entity_ids method.""" + hass.states.async_set("light.bowl", "on", {}) + hass.states.async_set("SWITCH.AC", "off", {}) + ent_ids = hass.states.async_entity_ids() + assert len(ent_ids) == 2 + assert "light.bowl" in ent_ids + assert "switch.ac" in ent_ids - def test_is_state(self): - """Test is_state method.""" - assert self.states.is_state("light.Bowl", "on") - assert not self.states.is_state("light.Bowl", "off") - assert not self.states.is_state("light.Non_existing", "on") + ent_ids = hass.states.async_entity_ids("light") + assert len(ent_ids) == 1 + assert "light.bowl" in ent_ids - def test_entity_ids(self): - """Test get_entity_ids method.""" - ent_ids = self.states.entity_ids() - assert len(ent_ids) == 2 - assert "light.bowl" in ent_ids - assert "switch.ac" in ent_ids + states = sorted(state.entity_id for state in hass.states.async_all()) + assert states == ["light.bowl", "switch.ac"] - ent_ids = self.states.entity_ids("light") - assert len(ent_ids) == 1 - assert "light.bowl" in ent_ids - def test_all(self): - """Test everything.""" - states = sorted(state.entity_id for state in self.states.all()) - assert ["light.bowl", "switch.ac"] == states +async def test_statemachine_remove(hass): + """Test remove method.""" + hass.states.async_set("light.bowl", "on", {}) + events = async_capture_events(hass, EVENT_STATE_CHANGED) - def test_remove(self): - """Test remove method.""" - events = [] + assert "light.bowl" in hass.states.async_entity_ids() + assert hass.states.async_remove("light.bowl") + await hass.async_block_till_done() - @ha.callback - def callback(event): - events.append(event) + assert "light.bowl" not in hass.states.async_entity_ids() + assert len(events) == 1 + assert events[0].data.get("entity_id") == "light.bowl" + assert events[0].data.get("old_state") is not None + assert events[0].data["old_state"].entity_id == "light.bowl" + assert events[0].data.get("new_state") is None - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + # If it does not exist, we should get False + assert not hass.states.async_remove("light.Bowl") + await hass.async_block_till_done() + assert len(events) == 1 - assert "light.bowl" in self.states.entity_ids() - assert self.states.remove("light.bowl") - self.hass.block_till_done() - assert "light.bowl" not in self.states.entity_ids() - assert len(events) == 1 - assert events[0].data.get("entity_id") == "light.bowl" - assert events[0].data.get("old_state") is not None - assert events[0].data["old_state"].entity_id == "light.bowl" - assert events[0].data.get("new_state") is None +async def test_statemachine_case_insensitivty(hass): + """Test insensitivty.""" + events = async_capture_events(hass, EVENT_STATE_CHANGED) - # If it does not exist, we should get False - assert not self.states.remove("light.Bowl") - self.hass.block_till_done() - assert len(events) == 1 + hass.states.async_set("light.BOWL", "off") + await hass.async_block_till_done() - def test_case_insensitivty(self): - """Test insensitivty.""" - runs = [] + assert hass.states.is_state("light.bowl", "off") + assert len(events) == 1 - @ha.callback - def callback(event): - runs.append(event) - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) +async def test_statemachine_last_changed_not_updated_on_same_state(hass): + """Test to not update the existing, same state.""" + hass.states.async_set("light.bowl", "on", {}) + state = hass.states.get("light.Bowl") - self.states.set("light.BOWL", "off") - self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(hours=10) - assert self.states.is_state("light.bowl", "off") - assert len(runs) == 1 + with patch("homeassistant.util.dt.utcnow", return_value=future): + hass.states.async_set("light.Bowl", "on", {"attr": "triggers_change"}) + await hass.async_block_till_done() - def test_last_changed_not_updated_on_same_state(self): - """Test to not update the existing, same state.""" - state = self.states.get("light.Bowl") + state2 = hass.states.get("light.Bowl") + assert state2 is not None + assert state.last_changed == state2.last_changed - future = dt_util.utcnow() + timedelta(hours=10) - with patch("homeassistant.util.dt.utcnow", return_value=future): - self.states.set("light.Bowl", "on", {"attr": "triggers_change"}) - self.hass.block_till_done() +async def test_statemachine_force_update(hass): + """Test force update option.""" + hass.states.async_set("light.bowl", "on", {}) + events = async_capture_events(hass, EVENT_STATE_CHANGED) - state2 = self.states.get("light.Bowl") - assert state2 is not None - assert state.last_changed == state2.last_changed + hass.states.async_set("light.bowl", "on") + await hass.async_block_till_done() + assert len(events) == 0 - def test_force_update(self): - """Test force update option.""" - events = [] - - @ha.callback - def callback(event): - events.append(event) - - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) - - self.states.set("light.bowl", "on") - self.hass.block_till_done() - assert len(events) == 0 - - self.states.set("light.bowl", "on", None, True) - self.hass.block_till_done() - assert len(events) == 1 + hass.states.async_set("light.bowl", "on", None, True) + await hass.async_block_till_done() + assert len(events) == 1 def test_service_call_repr(): @@ -675,202 +649,154 @@ def test_service_call_repr(): ) -class TestServiceRegistry(unittest.TestCase): - """Test ServicerRegistry methods.""" +async def test_serviceregistry_has_service(hass): + """Test has_service method.""" + hass.services.async_register("test_domain", "test_service", lambda call: None) + assert len(hass.services.async_services()) == 1 + assert hass.services.has_service("tesT_domaiN", "tesT_servicE") + assert not hass.services.has_service("test_domain", "non_existing") + assert not hass.services.has_service("non_existing", "test_service") - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.services = self.hass.services - @ha.callback - def mock_service(call): - pass +async def test_serviceregistry_call_with_blocking_done_in_time(hass): + """Test call with blocking.""" + registered_events = async_capture_events(hass, EVENT_SERVICE_REGISTERED) + calls = async_mock_service(hass, "test_domain", "register_calls") + await hass.async_block_till_done() - self.services.register("Test_Domain", "TEST_SERVICE", mock_service) + assert len(registered_events) == 1 + assert registered_events[0].data["domain"] == "test_domain" + assert registered_events[0].data["service"] == "register_calls" - self.calls_register = [] + assert await hass.services.async_call( + "test_domain", "REGISTER_CALLS", blocking=True + ) + assert len(calls) == 1 - @ha.callback - def mock_event_register(event): - """Mock register event.""" - self.calls_register.append(event) - self.hass.bus.listen(EVENT_SERVICE_REGISTERED, mock_event_register) +async def test_serviceregistry_call_non_existing_with_blocking(hass): + """Test non-existing with blocking.""" + with pytest.raises(ha.ServiceNotFound): + await hass.services.async_call("test_domain", "i_do_not_exist", blocking=True) - # pylint: disable=invalid-name - def tearDown(self): - """Stop down stuff we started.""" - self.hass.stop() - def test_has_service(self): - """Test has_service method.""" - assert self.services.has_service("tesT_domaiN", "tesT_servicE") - assert not self.services.has_service("test_domain", "non_existing") - assert not self.services.has_service("non_existing", "test_service") +async def test_serviceregistry_async_service(hass): + """Test registering and calling an async service.""" + calls = [] - def test_services(self): - """Test services.""" - assert len(self.services.services) == 1 + async def service_handler(call): + """Service handler coroutine.""" + calls.append(call) - def test_call_with_blocking_done_in_time(self): - """Test call with blocking.""" - calls = [] + hass.services.async_register("test_domain", "register_calls", service_handler) - @ha.callback - def service_handler(call): - """Service handler.""" - calls.append(call) + assert await hass.services.async_call( + "test_domain", "REGISTER_CALLS", blocking=True + ) + assert len(calls) == 1 - self.services.register("test_domain", "register_calls", service_handler) - self.hass.block_till_done() - assert len(self.calls_register) == 1 - assert self.calls_register[-1].data["domain"] == "test_domain" - assert self.calls_register[-1].data["service"] == "register_calls" +async def test_serviceregistry_async_service_partial(hass): + """Test registering and calling an wrapped async service.""" + calls = [] - assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True) - assert len(calls) == 1 + async def service_handler(call): + """Service handler coroutine.""" + calls.append(call) - def test_call_non_existing_with_blocking(self): - """Test non-existing with blocking.""" - with pytest.raises(ha.ServiceNotFound): - self.services.call("test_domain", "i_do_not_exist", blocking=True) + hass.services.async_register( + "test_domain", "register_calls", functools.partial(service_handler) + ) + await hass.async_block_till_done() - def test_async_service(self): - """Test registering and calling an async service.""" - calls = [] + assert await hass.services.async_call( + "test_domain", "REGISTER_CALLS", blocking=True + ) + assert len(calls) == 1 - async def service_handler(call): - """Service handler coroutine.""" - calls.append(call) - self.services.register("test_domain", "register_calls", service_handler) - self.hass.block_till_done() +async def test_serviceregistry_callback_service(hass): + """Test registering and calling an async service.""" + calls = [] - assert len(self.calls_register) == 1 - assert self.calls_register[-1].data["domain"] == "test_domain" - assert self.calls_register[-1].data["service"] == "register_calls" + @ha.callback + def service_handler(call): + """Service handler coroutine.""" + calls.append(call) - assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True) - self.hass.block_till_done() - assert len(calls) == 1 + hass.services.async_register("test_domain", "register_calls", service_handler) - def test_async_service_partial(self): - """Test registering and calling an wrapped async service.""" - calls = [] + assert await hass.services.async_call( + "test_domain", "REGISTER_CALLS", blocking=True + ) + assert len(calls) == 1 - async def service_handler(call): - """Service handler coroutine.""" - calls.append(call) - self.services.register( - "test_domain", "register_calls", functools.partial(service_handler) +async def test_serviceregistry_remove_service(hass): + """Test remove service.""" + calls_remove = async_capture_events(hass, EVENT_SERVICE_REMOVED) + + hass.services.async_register("test_domain", "test_service", lambda call: None) + assert hass.services.has_service("test_Domain", "test_Service") + + hass.services.async_remove("test_Domain", "test_Service") + await hass.async_block_till_done() + + assert not hass.services.has_service("test_Domain", "test_Service") + assert len(calls_remove) == 1 + assert calls_remove[-1].data["domain"] == "test_domain" + assert calls_remove[-1].data["service"] == "test_service" + + +async def test_serviceregistry_service_that_not_exists(hass): + """Test remove service that not exists.""" + calls_remove = async_capture_events(hass, EVENT_SERVICE_REMOVED) + assert not hass.services.has_service("test_xxx", "test_yyy") + hass.services.async_remove("test_xxx", "test_yyy") + await hass.async_block_till_done() + assert len(calls_remove) == 0 + + with pytest.raises(ServiceNotFound): + await hass.services.async_call("test_do_not", "exist", {}) + + +async def test_serviceregistry_async_service_raise_exception(hass): + """Test registering and calling an async service raise exception.""" + + async def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + hass.services.async_register("test_domain", "register_calls", service_handler) + + with pytest.raises(ValueError): + assert await hass.services.async_call( + "test_domain", "REGISTER_CALLS", blocking=True ) - self.hass.block_till_done() - assert len(self.calls_register) == 1 - assert self.calls_register[-1].data["domain"] == "test_domain" - assert self.calls_register[-1].data["service"] == "register_calls" + # Non-blocking service call never throw exception + await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False) + await hass.async_block_till_done() - assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True) - self.hass.block_till_done() - assert len(calls) == 1 - def test_callback_service(self): - """Test registering and calling an async service.""" - calls = [] +async def test_serviceregistry_callback_service_raise_exception(hass): + """Test registering and calling an callback service raise exception.""" - @ha.callback - def service_handler(call): - """Service handler coroutine.""" - calls.append(call) + @ha.callback + def service_handler(_): + """Service handler coroutine.""" + raise ValueError - self.services.register("test_domain", "register_calls", service_handler) - self.hass.block_till_done() + hass.services.async_register("test_domain", "register_calls", service_handler) - assert len(self.calls_register) == 1 - assert self.calls_register[-1].data["domain"] == "test_domain" - assert self.calls_register[-1].data["service"] == "register_calls" + with pytest.raises(ValueError): + assert await hass.services.async_call( + "test_domain", "REGISTER_CALLS", blocking=True + ) - assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True) - self.hass.block_till_done() - assert len(calls) == 1 - - def test_remove_service(self): - """Test remove service.""" - calls_remove = [] - - @ha.callback - def mock_event_remove(event): - """Mock register event.""" - calls_remove.append(event) - - self.hass.bus.listen(EVENT_SERVICE_REMOVED, mock_event_remove) - - assert self.services.has_service("test_Domain", "test_Service") - - self.services.remove("test_Domain", "test_Service") - self.hass.block_till_done() - - assert not self.services.has_service("test_Domain", "test_Service") - assert len(calls_remove) == 1 - assert calls_remove[-1].data["domain"] == "test_domain" - assert calls_remove[-1].data["service"] == "test_service" - - def test_remove_service_that_not_exists(self): - """Test remove service that not exists.""" - calls_remove = [] - - @ha.callback - def mock_event_remove(event): - """Mock register event.""" - calls_remove.append(event) - - self.hass.bus.listen(EVENT_SERVICE_REMOVED, mock_event_remove) - - assert not self.services.has_service("test_xxx", "test_yyy") - self.services.remove("test_xxx", "test_yyy") - self.hass.block_till_done() - assert len(calls_remove) == 0 - - def test_async_service_raise_exception(self): - """Test registering and calling an async service raise exception.""" - - async def service_handler(_): - """Service handler coroutine.""" - raise ValueError - - self.services.register("test_domain", "register_calls", service_handler) - self.hass.block_till_done() - - with pytest.raises(ValueError): - assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True) - self.hass.block_till_done() - - # Non-blocking service call never throw exception - self.services.call("test_domain", "REGISTER_CALLS", blocking=False) - self.hass.block_till_done() - - def test_callback_service_raise_exception(self): - """Test registering and calling an callback service raise exception.""" - - @ha.callback - def service_handler(_): - """Service handler coroutine.""" - raise ValueError - - self.services.register("test_domain", "register_calls", service_handler) - self.hass.block_till_done() - - with pytest.raises(ValueError): - assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True) - self.hass.block_till_done() - - # Non-blocking service call never throw exception - self.services.call("test_domain", "REGISTER_CALLS", blocking=False) - self.hass.block_till_done() + # Non-blocking service call never throw exception + await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False) + await hass.async_block_till_done() def test_config_defaults(): From f19f743bed243c34d6f86548a390a0080f360bd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Nov 2020 18:26:22 +0100 Subject: [PATCH 094/430] Update pytradfri to 7.0.3 (#43289) --- homeassistant/components/tradfri/manifest.json | 12 +++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index dace0f0739b..7b94bbf0bc0 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,15 +3,9 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": [ - "pytradfri[async]==7.0.2" - ], + "requirements": ["pytradfri[async]==7.0.3"], "homekit": { - "models": [ - "TRADFRI" - ] + "models": ["TRADFRI"] }, - "codeowners": [ - "@ggravlingen" - ] + "codeowners": ["@ggravlingen"] } diff --git a/requirements_all.txt b/requirements_all.txt index 95d49a43fb2..afbe79a8bfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1851,7 +1851,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.2 +pytradfri[async]==7.0.3 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb7dbbfe757..c8cb794ecfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -902,7 +902,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.2 +pytradfri[async]==7.0.3 # homeassistant.components.vera pyvera==0.3.11 From a164989e41f68556ab25cda1e95a2e7df52a5cd0 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 16 Nov 2020 18:53:42 +0100 Subject: [PATCH 095/430] Bump PyEssent to 0.14 (#43282) --- homeassistant/components/essent/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index a46d37ccdc8..c90ce5ba664 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -2,6 +2,6 @@ "domain": "essent", "name": "Essent", "documentation": "https://www.home-assistant.io/integrations/essent", - "requirements": ["PyEssent==0.13"], + "requirements": ["PyEssent==0.14"], "codeowners": ["@TheLastProject"] } diff --git a/requirements_all.txt b/requirements_all.txt index afbe79a8bfb..739d896fb2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -29,7 +29,7 @@ OPi.GPIO==0.4.0 Plugwise_Smile==1.6.0 # homeassistant.components.essent -PyEssent==0.13 +PyEssent==0.14 # homeassistant.components.flick_electric PyFlick==0.0.2 From 4ffba281db64fb7638c6f65b445a60eed43a57b3 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Mon, 16 Nov 2020 14:09:18 -0500 Subject: [PATCH 096/430] Bump env_canada to 0.2.4, fix config validation (#43251) --- homeassistant/components/environment_canada/camera.py | 2 +- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 87677e85065..66079ac73ff 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_STATION): cv.matches_regex(r"^C[A-Z]{4}$|^[A-Z]{3}$"), vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, - vol.Optional(CONF_PRECIP_TYPE): ["RAIN", "SNOW"], + vol.Optional(CONF_PRECIP_TYPE): vol.In(["RAIN", "SNOW"]), } ) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c605e92acd5..2a51c6ffd83 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.2"], + "requirements": ["env_canada==0.2.4"], "codeowners": ["@michaeldavie"] } diff --git a/requirements_all.txt b/requirements_all.txt index 739d896fb2a..2791255870a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -556,7 +556,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.2.2 +env_canada==0.2.4 # homeassistant.components.envirophat # envirophat==0.0.6 From 52e1282d8c569286c52a9fee79a6791ccc9819da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Nov 2020 20:10:55 +0100 Subject: [PATCH 097/430] Make MQTT climate return PRESET_NONE when no preset is set (#43257) --- homeassistant/components/mqtt/climate.py | 2 +- tests/components/mqtt/test_climate.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 68579559e35..8b762a82f02 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -640,7 +640,7 @@ class MqttClimate( return self._hold if self._away: return PRESET_AWAY - return None + return PRESET_NONE @property def preset_modes(self): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 3c629a82012..4d049753f43 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -438,11 +438,11 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async_fire_mqtt_message(hass, "away-state", "ON") state = hass.states.get(ENTITY_CLIMATE) @@ -450,11 +450,11 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, "away-state", "OFF") state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async_fire_mqtt_message(hass, "away-state", "nonsense") state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async def test_set_away_mode(hass, mqtt_mock): @@ -467,7 +467,7 @@ async def test_set_away_mode(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("away-mode-topic", "AN", 0, False) mqtt_mock.async_publish.reset_mock() @@ -477,7 +477,7 @@ async def test_set_away_mode(hass, mqtt_mock): await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("away-mode-topic", "AUS", 0, False) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) mqtt_mock.async_publish.reset_mock() @@ -525,7 +525,7 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, "hold-state", "off") state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async def test_set_hold(hass, mqtt_mock): @@ -534,7 +534,7 @@ async def test_set_hold(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("hold-topic", "hold-on", 0, False) mqtt_mock.async_publish.reset_mock() @@ -550,7 +550,7 @@ async def test_set_hold(hass, mqtt_mock): 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) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async def test_set_preset_mode_twice(hass, mqtt_mock): @@ -559,7 +559,7 @@ async def test_set_preset_mode_twice(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("hold-topic", "hold-on", 0, False) mqtt_mock.async_publish.reset_mock() @@ -735,7 +735,7 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): assert state.attributes.get("temperature") == 1031 # Away Mode - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async_fire_mqtt_message(hass, "away-state", '"ON"') state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "away" @@ -743,7 +743,7 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): # Away Mode with JSON values async_fire_mqtt_message(hass, "away-state", "false") state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") is None + assert state.attributes.get("preset_mode") == "none" async_fire_mqtt_message(hass, "away-state", "true") state = hass.states.get(ENTITY_CLIMATE) From 159ebe1dac265d2d264ad328aa1c8cdc56b0a1e1 Mon Sep 17 00:00:00 2001 From: reaper7 Date: Mon, 16 Nov 2020 20:25:07 +0100 Subject: [PATCH 098/430] Fix Enigma2 available entity property (#43292) Available property should return self.e2_box.is_offlin negation fixes previous PR: https://github.com/home-assistant/core/pull/42407 --- homeassistant/components/enigma2/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index cb58a3db333..8bb0486cd24 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -136,7 +136,7 @@ class Enigma2Device(MediaPlayerEntity): @property def available(self): """Return True if the device is available.""" - return self.e2_box.is_offline + return not self.e2_box.is_offline @property def supported_features(self): From 414f167508eaf64b8e5c8cc3c1ba397ae423d4f7 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 17 Nov 2020 04:13:33 +0800 Subject: [PATCH 099/430] Remove pts adjustments in stream (#42399) * Remove unnecessary pts adjustments * Add comments * Use -inf for initial last_dts to be more clear * Use video first_pts as common adjuster in recorder * Remove seek(0) before av.open --- homeassistant/components/stream/recorder.py | 30 ++++++--- homeassistant/components/stream/worker.py | 72 +++++++++------------ 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index d0b8789f602..420e7c654c5 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -25,12 +25,23 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma output_v = None output_a = None - for segment in segments: - # Seek to beginning and open segment - segment.segment.seek(0) + # Get first_pts values from first segment + if len(segments) > 0: + segment = segments[0] source = av.open(segment.segment, "r", format=container_format) source_v = source.streams.video[0] + first_pts["video"] = source_v.start_time + if len(source.streams.audio) > 0: + source_a = source.streams.audio[0] + first_pts["audio"] = int( + source_v.start_time * source_v.time_base / source_a.time_base + ) + source.close() + for segment in segments: + # Open segment + source = av.open(segment.segment, "r", format=container_format) + source_v = source.streams.video[0] # Add output streams if not output_v: output_v = output.add_stream(template=source_v) @@ -42,13 +53,12 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma # Remux video for packet in source.demux(): - if packet is not None and packet.dts is not None: - if first_pts[packet.stream.type] is None: - first_pts[packet.stream.type] = packet.pts - packet.pts -= first_pts[packet.stream.type] - packet.dts -= first_pts[packet.stream.type] - packet.stream = output_v if packet.stream.type == "video" else output_a - output.mux(packet) + if packet.dts is None: + continue + packet.pts -= first_pts[packet.stream.type] + packet.dts -= first_pts[packet.stream.type] + packet.stream = output_v if packet.stream.type == "video" else output_a + output.mux(packet) source.close() diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index aa6a5d350a9..68cbbc79726 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -102,11 +102,8 @@ def _stream_worker_internal(hass, stream, quit_event): # Iterator for demuxing container_packets = None - # The presentation timestamps of the first packet in each stream we receive - # Use to adjust before muxing or outputting, but we don't adjust internally - first_pts = {} # The decoder timestamps of the latest packet in each stream we processed - last_dts = None + last_dts = {video_stream: float("-inf"), audio_stream: float("-inf")} # Keep track of consecutive packets without a dts to detect end of stream. missing_dts = 0 # Holds the buffers for each stream provider @@ -123,21 +120,19 @@ def _stream_worker_internal(hass, stream, quit_event): # 2 - seeking can be problematic https://trac.ffmpeg.org/ticket/7815 def peek_first_pts(): - nonlocal first_pts, audio_stream, container_packets + """Initialize by peeking into the first few packets of the stream. + + Deal with problem #1 above (bad first packet pts/dts) by recalculating using pts/dts from second packet. + Also load the first video keyframe pts into segment_start_pts and check if the audio stream really exists. + """ + nonlocal segment_start_pts, audio_stream, container_packets missing_dts = 0 - - def empty_stream_dict(): - return { - video_stream: None, - **({audio_stream: None} if audio_stream else {}), - } - + found_audio = False try: container_packets = container.demux((video_stream, audio_stream)) - first_packet = empty_stream_dict() - first_pts = empty_stream_dict() + first_packet = None # Get to first video keyframe - while first_packet[video_stream] is None: + while first_packet is None: packet = next(container_packets) if ( packet.dts is None @@ -148,13 +143,17 @@ def _stream_worker_internal(hass, stream, quit_event): ) missing_dts += 1 continue - if packet.stream == video_stream and packet.is_keyframe: - first_packet[video_stream] = packet + if packet.stream == audio_stream: + found_audio = True + elif packet.is_keyframe: # video_keyframe + first_packet = packet initial_packets.append(packet) # Get first_pts from subsequent frame to first keyframe - while any( - [pts is None for pts in {**first_packet, **first_pts}.values()] - ) and (len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO): + while segment_start_pts is None or ( + audio_stream + and not found_audio + and len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO + ): packet = next(container_packets) if ( packet.dts is None @@ -165,24 +164,19 @@ def _stream_worker_internal(hass, stream, quit_event): ) missing_dts += 1 continue - if ( - first_packet[packet.stream] is None - ): # actually video already found above so only for audio - if packet.is_keyframe: - first_packet[packet.stream] = packet - else: # Discard leading non-keyframes - continue - else: # This is the second frame to calculate first_pts from - if first_pts[packet.stream] is None: - first_pts[packet.stream] = packet.dts - packet.duration - first_packet[packet.stream].pts = first_pts[packet.stream] - first_packet[packet.stream].dts = first_pts[packet.stream] + if packet.stream == audio_stream: + found_audio = True + elif ( + segment_start_pts is None + ): # This is the second video frame to calculate first_pts from + segment_start_pts = packet.dts - packet.duration + first_packet.pts = segment_start_pts + first_packet.dts = segment_start_pts initial_packets.append(packet) - if audio_stream and first_packet[audio_stream] is None: + if audio_stream and not found_audio: _LOGGER.warning( "Audio stream not found" ) # Some streams declare an audio stream and never send any packets - del first_pts[audio_stream] audio_stream = None except (av.AVError, StopIteration) as ex: @@ -212,9 +206,6 @@ def _stream_worker_internal(hass, stream, quit_event): ) def mux_video_packet(packet): - # adjust pts and dts before muxing - packet.pts -= first_pts[video_stream] - packet.dts -= first_pts[video_stream] # mux packets to each buffer for buffer, output_streams in outputs.values(): # Assign the packet to the new stream & mux @@ -223,9 +214,6 @@ def _stream_worker_internal(hass, stream, quit_event): def mux_audio_packet(packet): # almost the same as muxing video but add extra check - # adjust pts and dts before muxing - packet.pts -= first_pts[audio_stream] - packet.dts -= first_pts[audio_stream] for buffer, output_streams in outputs.values(): # Assign the packet to the new stream & mux if output_streams.get(audio_stream): @@ -241,8 +229,8 @@ def _stream_worker_internal(hass, stream, quit_event): if not peek_first_pts(): container.close() return - last_dts = {k: v - 1 for k, v in first_pts.items()} - initialize_segment(first_pts[video_stream]) + + initialize_segment(segment_start_pts) while not quit_event.is_set(): try: From c04b600dddc3031c87c3cc96677afb9a923e724a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Nov 2020 22:55:53 +0100 Subject: [PATCH 100/430] Update pytradfri to 7.0.4 (#43297) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 7b94bbf0bc0..7ffa8ed24bf 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.3"], + "requirements": ["pytradfri[async]==7.0.4"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 2791255870a..76faadfc186 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1851,7 +1851,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.3 +pytradfri[async]==7.0.4 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8cb794ecfd..48a3e8e6a0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -902,7 +902,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.3 +pytradfri[async]==7.0.4 # homeassistant.components.vera pyvera==0.3.11 From d11d1343a859f27873365d304309bba758964fa7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 16 Nov 2020 22:56:45 +0100 Subject: [PATCH 101/430] Updated frontend to 20201111.1 (#43298) --- 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 7355b40381b..893683dfb88 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201111.0"], + "requirements": ["home-assistant-frontend==20201111.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f80811b668f..c39bb3cc9a4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.37.2 -home-assistant-frontend==20201111.0 +home-assistant-frontend==20201111.1 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 76faadfc186..6a814e80d17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201111.0 +home-assistant-frontend==20201111.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48a3e8e6a0e..64202ba8995 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -397,7 +397,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201111.0 +home-assistant-frontend==20201111.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3e1f2a51034662349d342572b6a00d95590e34ad Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 16 Nov 2020 23:11:39 +0000 Subject: [PATCH 102/430] Refactor how entities are created for homekit_controller services (#43242) --- .../homekit_controller/air_quality.py | 7 +-- .../homekit_controller/alarm_control_panel.py | 7 +-- .../homekit_controller/binary_sensor.py | 19 +++---- .../components/homekit_controller/climate.py | 11 ++-- .../homekit_controller/connection.py | 12 ++--- .../components/homekit_controller/cover.py | 25 +++++---- .../homekit_controller/device_trigger.py | 16 +++--- .../components/homekit_controller/fan.py | 11 ++-- .../homekit_controller/humidifier.py | 52 +++++-------------- .../components/homekit_controller/light.py | 7 +-- .../components/homekit_controller/lock.py | 7 +-- .../homekit_controller/media_player.py | 6 +-- .../components/homekit_controller/sensor.py | 17 +++--- .../components/homekit_controller/switch.py | 13 ++--- 14 files changed, 94 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 999980ad60c..896034a2ca0 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -1,5 +1,6 @@ """Support for HomeKit Controller air quality sensors.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.air_quality import AirQualityEntity from homeassistant.core import callback @@ -85,10 +86,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - if service["stype"] != "air-quality": + def async_add_service(service): + if service.short_type != ServicesTypes.AIR_QUALITY_SENSOR: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeAirQualitySensor(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index bf3a5f27142..621fb01ff74 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,5 +1,6 @@ """Support for Homekit Alarm Control Panel.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( @@ -43,10 +44,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - if service["stype"] != "security-system": + def async_add_service(service): + if service.short_type != ServicesTypes.SECURITY_SYSTEM: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitAlarmControlPanelEntity(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index c718f7dc11a..537e9c2a698 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,5 +1,6 @@ """Support for Homekit motion sensors.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, @@ -124,12 +125,12 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): ENTITY_TYPES = { - "motion": HomeKitMotionSensor, - "contact": HomeKitContactSensor, - "smoke": HomeKitSmokeSensor, - "carbon-monoxide": HomeKitCarbonMonoxideSensor, - "occupancy": HomeKitOccupancySensor, - "leak": HomeKitLeakSensor, + ServicesTypes.MOTION_SENSOR: HomeKitMotionSensor, + ServicesTypes.CONTACT_SENSOR: HomeKitContactSensor, + ServicesTypes.SMOKE_SENSOR: HomeKitSmokeSensor, + ServicesTypes.CARBON_MONOXIDE_SENSOR: HomeKitCarbonMonoxideSensor, + ServicesTypes.OCCUPANCY_SENSOR: HomeKitOccupancySensor, + ServicesTypes.LEAK_SENSOR: HomeKitLeakSensor, } @@ -139,11 +140,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - entity_class = ENTITY_TYPES.get(service["stype"]) + def async_add_service(service): + entity_class = ENTITY_TYPES.get(service.short_type) if not entity_class: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 546e46d67cb..ed2d3c74d7d 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -10,6 +10,7 @@ from aiohomekit.model.characteristics import ( SwingModeValues, TargetHeaterCoolerStateValues, ) +from aiohomekit.model.services import ServicesTypes from aiohomekit.utils import clamp_enum_to_char from homeassistant.components.climate import ( @@ -87,11 +88,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - entity_class = ENTITY_TYPES.get(service["stype"]) + def async_add_service(service): + entity_class = ENTITY_TYPES.get(service.short_type) if not entity_class: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True @@ -454,6 +455,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): ENTITY_TYPES = { - "heater-cooler": HomeKitHeaterCoolerEntity, - "thermostat": HomeKitClimateEntity, + ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity, + ServicesTypes.THERMOSTAT: HomeKitClimateEntity, } diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 9ba6ef98a02..2b37d3e3d20 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -317,19 +317,17 @@ class HKDevice: self._add_new_entities_for_accessory(self.accessory_factories) def _add_new_entities(self, callbacks): - for accessory in self.accessories: - aid = accessory["aid"] - for service in accessory["services"]: - iid = service["iid"] - stype = ServicesTypes.get_short(service["type"].upper()) - service["stype"] = stype + for accessory in self.entity_map.accessories: + aid = accessory.aid + for service in accessory.services: + iid = service.iid if (aid, iid) in self.entities: # Don't add the same entity again continue for listener in callbacks: - if listener(aid, service): + if listener(service): self.entities.append((aid, iid)) break diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index a79ef5d1ee7..fdf48ebba5d 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,5 +1,6 @@ """Support for Homekit covers.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.cover import ( ATTR_POSITION, @@ -39,17 +40,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - info = {"aid": aid, "iid": service["iid"]} - if service["stype"] == "garage-door-opener": - async_add_entities([HomeKitGarageDoorCover(conn, info)], True) - return True - - if service["stype"] in ("window-covering", "window"): - async_add_entities([HomeKitWindowCover(conn, info)], True) - return True - - return False + def async_add_service(service): + entity_class = ENTITY_TYPES.get(service.short_type) + if not entity_class: + return False + info = {"aid": service.accessory.aid, "iid": service.iid} + async_add_entities([entity_class(conn, info)], True) + return True conn.add_listener(async_add_service) @@ -246,3 +243,9 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity): if not obstruction_detected: return {} return {"obstruction-detected": obstruction_detected} + + +ENTITY_TYPES = { + ServicesTypes.GARAGE_DOOR_OPENER: HomeKitGarageDoorCover, + ServicesTypes.WINDOW_COVERING: HomeKitWindowCover, +} diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 76b82eec597..b2e668915d7 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -174,9 +174,9 @@ def enumerate_doorbell(service): TRIGGER_FINDERS = { - "service-label": enumerate_stateless_switch_group, - "stateless-programmable-switch": enumerate_stateless_switch, - "doorbell": enumerate_doorbell, + ServicesTypes.SERVICE_LABEL: enumerate_stateless_switch_group, + ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: enumerate_stateless_switch, + ServicesTypes.DOORBELL: enumerate_doorbell, } @@ -186,8 +186,9 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service_dict): - service_type = service_dict["stype"] + def async_add_service(service): + aid = service.accessory.aid + service_type = service.short_type # If not a known service type then we can't handle any stateless events for it if service_type not in TRIGGER_FINDERS: @@ -201,11 +202,6 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): if device_id in hass.data[TRIGGERS]: return False - # At the moment add_listener calls us with the raw service dict, rather than - # a service model. So turn it into a service ourselves. - accessory = conn.entity_map.aid(aid) - service = accessory.services.iid(service_dict["iid"]) - # Just because we recognise the service type doesn't mean we can actually # extract any triggers - so only proceed if we can triggers = TRIGGER_FINDERS[service_type](service) diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 5fe119f65b7..476d0f2c8e5 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -1,5 +1,6 @@ """Support for Homekit fans.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.fan import ( DIRECTION_FORWARD, @@ -161,8 +162,8 @@ class HomeKitFanV2(BaseHomeKitFan): ENTITY_TYPES = { - "fan": HomeKitFanV1, - "fanv2": HomeKitFanV2, + ServicesTypes.FAN: HomeKitFanV1, + ServicesTypes.FAN_V2: HomeKitFanV2, } @@ -172,11 +173,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - entity_class = ENTITY_TYPES.get(service["stype"]) + def async_add_service(service): + entity_class = ENTITY_TYPES.get(service.short_type) if not entity_class: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 10ffee198e4..e4bed25d618 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -2,6 +2,7 @@ from typing import List, Optional from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.humidifier import HumidifierEntity from homeassistant.components.humidifier.const import ( @@ -253,51 +254,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] - def get_accessory(conn, aid): - for acc in conn.accessories: - if acc.get("aid") == aid: - return acc - return None - - def get_service(acc, iid): - for serv in acc.get("services"): - if serv.get("iid") == iid: - return serv - return None - - def get_char(serv, iid): - try: - type_name = CharacteristicsTypes[iid] - type_uuid = CharacteristicsTypes.get_uuid(type_name) - for char in serv.get("characteristics"): - if char.get("type") == type_uuid: - return char - except KeyError: - return None - return None - @callback - def async_add_service(aid, service): - if service["stype"] != "humidifier-dehumidifier": + def async_add_service(service): + if service.short_type != ServicesTypes.HUMIDIFIER_DEHUMIDIFIER: return False - info = {"aid": aid, "iid": service["iid"]} - acc = get_accessory(conn, aid) - serv = get_service(acc, service["iid"]) + info = {"aid": service.accessory.aid, "iid": service.iid} - if ( - get_char(serv, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD) - is not None - ): - async_add_entities([HomeKitHumidifier(conn, info)], True) + entities = [] - if ( - get_char( - serv, CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ) - is not None - ): - async_add_entities([HomeKitDehumidifier(conn, info)], True) + if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD): + entities.append(HomeKitHumidifier(conn, info)) + + if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD): + entities.append(HomeKitDehumidifier(conn, info)) + + async_add_entities(entities, True) return True diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 3f9f6f3de29..497131327b6 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,5 +1,6 @@ """Support for Homekit lights.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -21,10 +22,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - if service["stype"] != "lightbulb": + def async_add_service(service): + if service.short_type != ServicesTypes.LIGHTBULB: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitLight(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 52e9947d986..8ac7fd608fd 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,5 +1,6 @@ """Support for HomeKit Controller locks.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED @@ -20,10 +21,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - if service["stype"] != "lock-mechanism": + def async_add_service(service): + if service.short_type != ServicesTypes.LOCK_MECHANISM: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitLock(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 2ffc794409b..6dfa8720ee5 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -44,10 +44,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - if service["stype"] != "television": + def async_add_service(service): + if service.short_type != ServicesTypes.TELEVISION: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitTelevision(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 2075eb9dcc3..677f1bc67f1 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,5 +1,6 @@ """Support for Homekit sensors.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -216,11 +217,11 @@ class HomeKitBatterySensor(HomeKitEntity): ENTITY_TYPES = { - "humidity": HomeKitHumiditySensor, - "temperature": HomeKitTemperatureSensor, - "light": HomeKitLightSensor, - "carbon-dioxide": HomeKitCarbonDioxideSensor, - "battery": HomeKitBatterySensor, + ServicesTypes.HUMIDITY_SENSOR: HomeKitHumiditySensor, + ServicesTypes.TEMPERATURE_SENSOR: HomeKitTemperatureSensor, + ServicesTypes.LIGHT_SENSOR: HomeKitLightSensor, + ServicesTypes.CARBON_DIOXIDE_SENSOR: HomeKitCarbonDioxideSensor, + ServicesTypes.BATTERY_SERVICE: HomeKitBatterySensor, } @@ -230,11 +231,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - entity_class = ENTITY_TYPES.get(service["stype"]) + def async_add_service(service): + entity_class = ENTITY_TYPES.get(service.short_type) if not entity_class: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 3408d036b58..b9d0b273cb1 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -4,6 +4,7 @@ from aiohomekit.model.characteristics import ( InUseValues, IsConfiguredValues, ) +from aiohomekit.model.services import ServicesTypes from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback @@ -96,9 +97,9 @@ class HomeKitValve(HomeKitEntity, SwitchEntity): ENTITY_TYPES = { - "switch": HomeKitSwitch, - "outlet": HomeKitSwitch, - "valve": HomeKitValve, + ServicesTypes.SWITCH: HomeKitSwitch, + ServicesTypes.OUTLET: HomeKitSwitch, + ServicesTypes.VALVE: HomeKitValve, } @@ -108,11 +109,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(aid, service): - entity_class = ENTITY_TYPES.get(service["stype"]) + def async_add_service(service): + entity_class = ENTITY_TYPES.get(service.short_type) if not entity_class: return False - info = {"aid": aid, "iid": service["iid"]} + info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True From 699402ca18f31a125eb81670a5b6e425814fe9e2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 16 Nov 2020 16:31:35 -0700 Subject: [PATCH 103/430] Bump aioguardian to 1.0.4 (#43299) --- homeassistant/components/guardian/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 31572a2ae49..f1fa9c73e5d 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", "requirements": [ - "aioguardian==1.0.1" + "aioguardian==1.0.4" ], "zeroconf": [ "_api._udp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 6a814e80d17..4fa053e1cb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.guardian -aioguardian==1.0.1 +aioguardian==1.0.4 # homeassistant.components.harmony aioharmony==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64202ba8995..4ae60f2db8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioflo==0.4.1 aiofreepybox==0.0.8 # homeassistant.components.guardian -aioguardian==1.0.1 +aioguardian==1.0.4 # homeassistant.components.harmony aioharmony==0.2.6 From 096d73eb4f9d299ba38300ff86fc8929e224e442 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 17 Nov 2020 00:31:54 +0100 Subject: [PATCH 104/430] Bump bimmer_connected to 0.7.13 (#43294) Co-authored-by: rikroe --- 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 ad073e55930..cb17459e105 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,6 +2,6 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.12"], + "requirements": ["bimmer_connected==0.7.13"], "codeowners": ["@gerard33", "@rikroe"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4fa053e1cb1..c447d10a54e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -345,7 +345,7 @@ beautifulsoup4==4.9.1 bellows==0.20.3 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.12 +bimmer_connected==0.7.13 # homeassistant.components.bizkaibus bizkaibus==0.1.1 From c8113e6b11d79bdcd6ead8140f3bd6d871bc2cd6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 17 Nov 2020 00:06:49 +0000 Subject: [PATCH 105/430] [ci skip] Translation update --- homeassistant/components/hassio/translations/es.json | 1 + homeassistant/components/shelly/translations/nl.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 4c8223f606b..9e874319246 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -5,6 +5,7 @@ "disk_total": "Disco total", "disk_used": "Disco usado", "docker_version": "Versi\u00f3n de Docker", + "healthy": "Saludable", "host_os": "Sistema operativo host", "installed_addons": "Complementos instalados", "supervisor_api": "API del Supervisor", diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index ecdfa41e109..75a2d2771d6 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -11,7 +11,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "Wilt u het {model} bij {host} instellen? Voordat het apparaat op batterijen kan worden ingesteld, moet het worden gewekt door op de knop op het apparaat te drukken." + "description": "Wilt u het {model} bij {host} instellen? Voordat apparaten op batterijen kunnen worden ingesteld, moet het worden gewekt door op de knop op het apparaat te drukken." }, "credentials": { "data": { From ed36cb7bebf4c55003c54e42417da801edf18e83 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 17 Nov 2020 02:54:44 +0100 Subject: [PATCH 106/430] Add notification binary_sensor to Plugwise integration (#41473) * Notifications extract from beta * Remove info loggings * Delete notification service * Only notifications for right smiles * Revert to correct logic * Catchup with dev (mostly with ourselves from #41201) * Remove debug logging * Naming improvement * Improve test quality as per codecov patch requirement * Revert to original condition (and appropriately test) * Fix delete_notification_service, bring tests fixtures up to 1.6.0 including notifications * Review comment (@bouwew) * Correct test value * Re-apply #40108 fix after rebase sidestep * Update tests/components/plugwise/test_init.py Co-authored-by: Chris Talkington * Add needed state to imports * Remove separate gw unload code * Change entry_fail approach * Revert persistent notification part * Revert persistent notification part - lint * Update homeassistant/components/plugwise/binary_sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/plugwise/binary_sensor.py Co-authored-by: Martin Hjelmare * Rework reuse of sensor in binary_sensor * Explicit state attribute keys * Remove tempfile * List of notifications per severity * Update homeassistant/components/plugwise/binary_sensor.py Co-authored-by: Martin Hjelmare Co-authored-by: Chris Talkington Co-authored-by: Martin Hjelmare --- .../components/plugwise/binary_sensor.py | 96 ++++++++++++++++--- homeassistant/components/plugwise/const.py | 2 + tests/components/plugwise/conftest.py | 6 ++ .../components/plugwise/test_binary_sensor.py | 12 +++ tests/components/plugwise/test_init.py | 18 +++- tests/components/plugwise/test_sensor.py | 15 ++- .../get_all_devices.json | 2 +- .../680423ff840043738f42cc7f1ff97a36.json | 2 +- .../6a3bf693d05e48e0b460c815a4fdd09d.json | 2 +- .../90986d591dcd426cae3ec3e8111ff730.json | 2 +- .../a2c3583e0a6349358998b760cea82d2a.json | 2 +- .../b310b72a0e354bfab43089919b9a88bf.json | 2 +- .../b59bcebaf94b499ea7d46e4a66fb62d8.json | 2 +- .../d3da73bde12a47d5a6b8f9dad971f2ec.json | 2 +- .../df4a4a8169904cdb9c03d61a21f42140.json | 2 +- .../e7693eb9582644e5b865dba8d4447cf1.json | 2 +- .../f1fee6043d3642a9b0a65297455f008e.json | 2 +- .../notifications.json | 1 + .../anna_heatpump/get_all_devices.json | 2 +- .../1cbf783bb11e4a7c8a6843dee3a86927.json | 2 +- .../3cb70739631c4d17a86b8b12e8a5161b.json | 2 +- .../plugwise/anna_heatpump/notifications.json | 1 + .../p1v3_full_option/get_all_devices.json | 2 +- .../e950c7d5e1ee407a858e2a8b5016c8b3.json | 2 +- .../p1v3_full_option/notifications.json | 1 + 25 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/notifications.json create mode 100644 tests/fixtures/plugwise/p1v3_full_option/notifications.json diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 2ba85326265..825d27d59bb 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -3,7 +3,6 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from .const import ( @@ -13,13 +12,16 @@ from .const import ( FLOW_OFF_ICON, FLOW_ON_ICON, IDLE_ICON, + NO_NOTIFICATION_ICON, + NOTIFICATION_ICON, ) -from .sensor import SmileSensor +from .gateway import SmileGateway BINARY_SENSOR_MAP = { "dhw_state": ["Domestic Hot Water State", None], "slave_boiler_state": ["Secondary Heater Device State", None], } +SEVERITIES = ["other", "info", "warning", "error"] _LOGGER = logging.getLogger(__name__) @@ -30,12 +32,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] entities = [] + is_thermostat = api.single_master_thermostat() all_devices = api.get_all_devices() for dev_id, device_properties in all_devices.items(): + if device_properties["class"] == "heater_central": data = api.get_device_data(dev_id) - for binary_sensor in BINARY_SENSOR_MAP: if binary_sensor not in data: continue @@ -47,32 +50,65 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device_properties["name"], dev_id, binary_sensor, - device_properties["class"], ) ) + if device_properties["class"] == "gateway" and is_thermostat is not None: + entities.append( + PwNotifySensor( + api, + coordinator, + device_properties["name"], + dev_id, + "plugwise_notification", + ) + ) + async_add_entities(entities, True) -class PwBinarySensor(SmileSensor, BinarySensorEntity): - """Representation of a Plugwise binary_sensor.""" +class SmileBinarySensor(SmileGateway): + """Represent Smile Binary Sensors.""" - def __init__(self, api, coordinator, name, dev_id, binary_sensor, model): - """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id, binary_sensor) + def __init__(self, api, coordinator, name, dev_id, binary_sensor): + """Initialise the binary_sensor.""" + super().__init__(api, coordinator, name, dev_id) self._binary_sensor = binary_sensor - self._is_on = False self._icon = None + self._is_on = False + + if dev_id == self._api.heater_id: + self._entity_name = "Auxiliary" + + sensorname = binary_sensor.replace("_", " ").title() + self._name = f"{self._entity_name} {sensorname}" + + if dev_id == self._api.gateway_id: + self._entity_name = f"Smile {self._entity_name}" self._unique_id = f"{dev_id}-{binary_sensor}" + @property + def icon(self): + """Return the icon of this entity.""" + return self._icon + @property def is_on(self): """Return true if the binary sensor is on.""" return self._is_on + @callback + def _async_process_data(self): + """Update the entity.""" + raise NotImplementedError + + +class PwBinarySensor(SmileBinarySensor, BinarySensorEntity): + """Representation of a Plugwise binary_sensor.""" + @callback def _async_process_data(self): """Update the entity.""" @@ -89,10 +125,48 @@ class PwBinarySensor(SmileSensor, BinarySensorEntity): self._is_on = data[self._binary_sensor] - self._state = STATE_ON if self._is_on else STATE_OFF if self._binary_sensor == "dhw_state": self._icon = FLOW_ON_ICON if self._is_on else FLOW_OFF_ICON if self._binary_sensor == "slave_boiler_state": self._icon = FLAME_ICON if self._is_on else IDLE_ICON self.async_write_ha_state() + + +class PwNotifySensor(SmileBinarySensor, BinarySensorEntity): + """Representation of a Plugwise Notification binary_sensor.""" + + def __init__(self, api, coordinator, name, dev_id, binary_sensor): + """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id, binary_sensor) + + self._attributes = {} + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @callback + def _async_process_data(self): + """Update the entity.""" + notify = self._api.notifications + + for severity in SEVERITIES: + self._attributes[f"{severity}_msg"] = [] + + self._is_on = False + self._icon = NO_NOTIFICATION_ICON + + if notify: + self._is_on = True + self._icon = NOTIFICATION_ICON + + for details in notify.values(): + for msg_type, msg in details.items(): + if msg_type not in SEVERITIES: + msg_type = "other" + + self._attributes[f"{msg_type.lower()}_msg"].append(msg) + + self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 5c0cf2b097a..f965676aef2 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -42,6 +42,8 @@ FLOW_OFF_ICON = "mdi:water-pump-off" FLOW_ON_ICON = "mdi:water-pump" IDLE_ICON = "mdi:circle-off-outline" SWITCH_ICON = "mdi:electric-switch" +NO_NOTIFICATION_ICON = "mdi:mailbox-outline" +NOTIFICATION_ICON = "mdi:mailbox-up-outline" COORDINATOR = "coordinator" UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 0f0e4551b1c..938e61146e5 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -75,6 +75,8 @@ def mock_smile_adam(): smile_mock.return_value.smile_type = "thermostat" smile_mock.return_value.smile_hostname = "smile98765" + smile_mock.return_value.notifications = _read_json(chosen_env, "notifications") + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( return_value=True @@ -118,6 +120,8 @@ def mock_smile_anna(): smile_mock.return_value.smile_type = "thermostat" smile_mock.return_value.smile_hostname = "smile98765" + smile_mock.return_value.notifications = _read_json(chosen_env, "notifications") + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( return_value=True @@ -161,6 +165,8 @@ def mock_smile_p1(): smile_mock.return_value.smile_type = "power" smile_mock.return_value.smile_hostname = "smile98765" + smile_mock.return_value.notifications = _read_json(chosen_env, "notifications") + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( return_value=True diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index b2221194d8e..6df5b90878a 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -35,3 +35,15 @@ async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): state = hass.states.get("binary_sensor.auxiliary_dhw_state") assert str(state.state) == STATE_OFF + + +async def test_adam_climate_binary_sensor_change(hass, mock_smile_adam): + """Test change of climate related binary_sensor entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("binary_sensor.adam_plugwise_notification") + assert str(state.state) == STATE_ON + assert "unreachable" in state.attributes.get("warning_msg")[0] + assert not state.attributes.get("error_msg") + assert not state.attributes.get("other_msg") diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index d968f1825f0..db7a71d660b 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -5,13 +5,13 @@ import asyncio from Plugwise_Smile.Smile import Smile from homeassistant.components.plugwise import DOMAIN -from homeassistant.components.plugwise.gateway import async_unload_entry from homeassistant.config_entries import ( + ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from tests.common import AsyncMock +from tests.common import AsyncMock, MockConfigEntry from tests.components.plugwise.common import async_init_integration @@ -53,5 +53,17 @@ async def test_unload_entry(hass, mock_smile_adam): entry = await async_init_integration(hass, mock_smile_adam) mock_smile_adam.async_reset = AsyncMock(return_value=True) - assert await async_unload_entry(hass, entry) + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ENTRY_STATE_NOT_LOADED assert not hass.data[DOMAIN] + + +async def test_async_setup_entry_fail(hass): + """Test async_setup_entry.""" + entry = MockConfigEntry(domain=DOMAIN, data={}) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ENTRY_STATE_SETUP_ERROR diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index a727337d747..a722749496f 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ENTRY_STATE_LOADED +from tests.common import Mock from tests.components.plugwise.common import async_init_integration @@ -30,7 +31,7 @@ async def test_adam_climate_sensor_entities(hass, mock_smile_adam): assert int(state.state) == 34 -async def test_anna_climate_sensor_entities(hass, mock_smile_anna): +async def test_anna_as_smt_climate_sensor_entities(hass, mock_smile_anna): """Test creation of climate related sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) assert entry.state == ENTRY_STATE_LOADED @@ -45,6 +46,16 @@ async def test_anna_climate_sensor_entities(hass, mock_smile_anna): assert float(state.state) == 86.0 +async def test_anna_climate_sensor_entities(hass, mock_smile_anna): + """Test creation of climate related sensor entities as single master thermostat.""" + mock_smile_anna.single_master_thermostat.side_effect = Mock(return_value=False) + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.auxiliary_outdoor_temperature") + assert float(state.state) == 18.0 + + async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): """Test creation of power related sensor entities.""" entry = await async_init_integration(hass, mock_smile_p1) @@ -63,7 +74,7 @@ async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): assert float(state.state) == 442.9 state = hass.states.get("sensor.p1_gas_consumed_cumulative") - assert float(state.state) == 584.9 + assert float(state.state) == 584.85 async def test_stretch_sensor_entities(hass, mock_stretch): diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json index 61ebc4d9a6c..bcaf40b4196 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json @@ -1 +1 @@ -{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file +{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json index 75bc62fbad4..6754cf63d2d 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json @@ -1 +1 @@ -{"setpoint": 14.0, "temperature": 19.1, "battery": 0.51, "valve_position": 0.0, "temperature_difference": -0.4} \ No newline at end of file +{"temperature": 19.1, "setpoint": 14.0, "battery": 0.51, "temperature_difference": -0.4, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json index 41333f374e1..14d596fb315 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json @@ -1 +1 @@ -{"setpoint": 15.0, "temperature": 17.2, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 15.0, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file +{"temperature": 17.2, "setpoint": 15.0, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 16.5, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json index 5e481d36b46..862a3159754 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json @@ -1 +1 @@ -{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01} \ No newline at end of file +{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01, "heating_state": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json index eef83a67a20..c3e1a35b292 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json @@ -1 +1 @@ -{"setpoint": 13.0, "temperature": 17.2, "battery": 0.62, "valve_position": 0.0, "temperature_difference": -0.2} \ No newline at end of file +{"temperature": 17.2, "setpoint": 13.0, "battery": 0.62, "temperature_difference": -0.2, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json index 16da5f44ef5..8478716dc7b 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json @@ -1 +1 @@ -{"setpoint": 21.5, "temperature": 26.0, "valve_position": 1.0, "temperature_difference": 3.5} \ No newline at end of file +{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 1.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json index 65fa0dd3d52..6d1a8d135a4 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json @@ -1 +1 @@ -{"setpoint": 21.5, "temperature": 20.9, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file +{"temperature": 20.9, "setpoint": 21.5, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json index fd202e05586..b5a26000c7f 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json @@ -1 +1 @@ -{"setpoint": 15.0, "temperature": 17.1, "battery": 0.62, "valve_position": 0.0, "temperature_difference": 0.1} \ No newline at end of file +{"temperature": 17.1, "setpoint": 15.0, "battery": 0.62, "temperature_difference": 0.1, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json index 12947c42ce0..f27c382fc0b 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json @@ -1 +1 @@ -{"setpoint": 13.0, "temperature": 16.5, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file +{"temperature": 16.5, "setpoint": 13.0, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json index 151b4b41f70..610c019b686 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json @@ -1 +1 @@ -{"setpoint": 5.5, "temperature": 15.6, "battery": 0.68, "valve_position": 0.0, "temperature_difference": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file +{"temperature": 15.6, "setpoint": 5.5, "battery": 0.68, "temperature_difference": 0.0, "valve_position": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json index 9934e109033..c4b5769e6d1 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json @@ -1 +1 @@ -{"setpoint": 14.0, "temperature": 18.9, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file +{"temperature": 18.9, "setpoint": 14.0, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json new file mode 100644 index 00000000000..c229f64da04 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json @@ -0,0 +1 @@ +{"af82e4ccf9c548528166d38e560662a4": {"warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device."}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json index 4992a175b14..191f5b442b7 100644 --- a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json +++ b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json @@ -1 +1 @@ -{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file +{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json index a8aea8e1357..ddf807303a2 100644 --- a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json @@ -1 +1 @@ -{"outdoor_temperature": 18.0, "heating_state": false, "dhw_state": false, "water_temperature": 29.1, "return_temperature": 25.1, "water_pressure": 1.57, "intended_boiler_temperature": 0.0, "modulation_level": 0.52, "cooling_state": false, "slave_boiler_state": false, "compressor_state": true, "flame_state": false} \ No newline at end of file +{"water_temperature": 29.1, "dhw_state": false, "intended_boiler_temperature": 0.0, "heating_state": false, "modulation_level": 0.52, "return_temperature": 25.1, "compressor_state": true, "cooling_state": false, "slave_boiler_state": false, "flame_state": false, "water_pressure": 1.57, "outdoor_temperature": 18.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json index 2a092e792d5..3177880705b 100644 --- a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json @@ -1 +1 @@ -{"setpoint": 21.0, "temperature": 23.3, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file +{"temperature": 23.3, "setpoint": 21.0, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/notifications.json b/tests/fixtures/plugwise/anna_heatpump/notifications.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/notifications.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json index e25fcb953c8..1feb33dd630 100644 --- a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json +++ b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json @@ -1 +1 @@ -{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["power", "home"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file +{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["home", "power"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json index 36cb66c7902..fcbc1bbce33 100644 --- a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json +++ b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json @@ -1 +1 @@ -{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.9, "gas_consumed_interval": 0.0} \ No newline at end of file +{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.85, "gas_consumed_interval": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/notifications.json b/tests/fixtures/plugwise/p1v3_full_option/notifications.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/tests/fixtures/plugwise/p1v3_full_option/notifications.json @@ -0,0 +1 @@ +{} \ No newline at end of file From c861a1c850a610cf4e135a5203a25a14ba49fe16 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 16 Nov 2020 23:50:04 -0600 Subject: [PATCH 107/430] Update directv to 0.4.0 (#43302) * update directv to 0.4.0 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/directv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index e4be9cc3e25..91685553596 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -2,7 +2,7 @@ "domain": "directv", "name": "DirecTV", "documentation": "https://www.home-assistant.io/integrations/directv", - "requirements": ["directv==0.3.0"], + "requirements": ["directv==0.4.0"], "codeowners": ["@ctalkington"], "quality_scale": "gold", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index c447d10a54e..a42c3829d0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -490,7 +490,7 @@ denonavr==0.9.5 devolo-home-control-api==0.16.0 # homeassistant.components.directv -directv==0.3.0 +directv==0.4.0 # homeassistant.components.discogs discogs_client==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ae60f2db8c..ba665b3b709 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -263,7 +263,7 @@ denonavr==0.9.5 devolo-home-control-api==0.16.0 # homeassistant.components.directv -directv==0.3.0 +directv==0.4.0 # homeassistant.components.updater distro==1.5.0 From 95504b7408531ce56e4a8ddbaeca65ec5a8337fd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 17 Nov 2020 01:11:42 -0500 Subject: [PATCH 108/430] Abort vizio discovery flow without unique ID (#43303) * abort vizio discovery flow if unique ID cant be found because it means we cant connect * add tests * fix abort call --- homeassistant/components/vizio/config_flow.py | 4 ++++ homeassistant/components/vizio/strings.json | 1 + tests/components/vizio/test_config_flow.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index b6d6d9bfb05..40f71adda12 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -369,6 +369,10 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info[CONF_DEVICE_CLASS], session=async_get_clientsession(self.hass, False), ) + + if not unique_id: + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id(unique_id=unique_id, raise_on_progress=True) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index da039b6a89e..adad8406da0 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -34,6 +34,7 @@ }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." } }, diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 32469fabd05..e966188afd2 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -856,6 +856,21 @@ async def test_zeroconf_ignore( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM +async def test_zeroconf_no_unique_id( + hass: HomeAssistantType, + vizio_no_unique_id: pytest.fixture, +) -> None: + """Test zeroconf discovery aborts when unique_id is None.""" + + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + async def test_zeroconf_abort_when_ignored( hass: HomeAssistantType, vizio_connect: pytest.fixture, From 4e6035d057bfcd7a9ffff173cc80075d56abb6b7 Mon Sep 17 00:00:00 2001 From: Thomas <71447672+ttuffin@users.noreply.github.com> Date: Tue, 17 Nov 2020 07:34:20 +0100 Subject: [PATCH 109/430] Improvement to allow parsing of station ID in vasttrafik integration. Addresses #34851 (#43136) --- homeassistant/components/vasttrafik/sensor.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 2b904faab4d..8b1609be6ba 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -80,14 +80,23 @@ class VasttrafikDepartureSensor(Entity): """Initialize the sensor.""" self._planner = planner self._name = name or departure - self._departure = planner.location_name(departure)[0] - self._heading = planner.location_name(heading)[0] if heading else None + self._departure = self.get_station_id(departure) + self._heading = self.get_station_id(heading) if heading else None self._lines = lines if lines else None self._delay = timedelta(minutes=delay) self._departureboard = None self._state = None self._attributes = None + def get_station_id(self, location): + """Get the station ID.""" + if location.isdecimal(): + station_info = {"station_name": location, "station_id": location} + else: + station_id = self._planner.location_name(location)[0]["id"] + station_info = {"station_name": location, "station_id": station_id} + return station_info + @property def name(self): """Return the name of the sensor.""" @@ -113,8 +122,8 @@ class VasttrafikDepartureSensor(Entity): """Get the departure board.""" try: self._departureboard = self._planner.departureboard( - self._departure["id"], - direction=self._heading["id"] if self._heading else None, + self._departure["station_id"], + direction=self._heading["station_id"] if self._heading else None, date=now() + self._delay, ) except vasttrafik.Error: @@ -123,9 +132,9 @@ class VasttrafikDepartureSensor(Entity): if not self._departureboard: _LOGGER.debug( - "No departures from %s heading %s", - self._departure["name"], - self._heading["name"] if self._heading else "ANY", + "No departures from departure station %s " "to destination station %s", + self._departure["station_name"], + self._heading["station_name"] if self._heading else "ANY", ) self._state = None self._attributes = {} From aa6376a69174bea235e047ee6a674bcb85e046c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Nov 2020 08:55:56 +0100 Subject: [PATCH 110/430] Bump codecov/codecov-action from v1.0.14 to v1.0.15 (#43304) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.0.14 to v1.0.15. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Commits](https://github.com/codecov/codecov-action/compare/v1.0.14...239febf655bba88b16ff5dea1d3135ea8663a1f9) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 48e4244600b..4e8cba12564 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -785,4 +785,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.0.14 + uses: codecov/codecov-action@v1.0.15 From e73d2c65cd1efce633067a8372a610603c08ed90 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 17 Nov 2020 11:44:06 +0100 Subject: [PATCH 111/430] Add progress translation key to hassfest (#43311) --- script/hassfest/translations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 17da80c3e8a..75886eedc6f 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -98,6 +98,7 @@ def gen_data_entry_schema( }, vol.Optional("error"): {str: cv.string_with_no_html}, vol.Optional("abort"): {str: cv.string_with_no_html}, + vol.Optional("progress"): {str: cv.string_with_no_html}, vol.Optional("create_entry"): {str: cv.string_with_no_html}, } if flow_title == REQUIRED: From b358103b58860e496a1702437ed440d0649592fa Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 17 Nov 2020 16:31:59 +0100 Subject: [PATCH 112/430] Update cloud integration to 0.38.0 (#43314) Co-authored-by: Paulus Schoutsen --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/tts.py | 35 +++++++++++++++----- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_tts.py | 15 +++++++++ 6 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 tests/components/cloud/test_tts.py diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index f3c79a470ea..3f65ed2ba46 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.37.2"], + "requirements": ["hass-nabucasa==0.38.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index ea769c6a054..9dd392a12c5 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 import Cloud -from hass_nabucasa.voice import VoiceError +from hass_nabucasa.voice import MAP_VOICE, VoiceError import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -10,17 +10,36 @@ from .const import DOMAIN CONF_GENDER = "gender" -SUPPORT_LANGUAGES = ["en-US", "de-DE", "es-ES"] -SUPPORT_GENDER = ["male", "female"] +SUPPORT_LANGUAGES = list({key[0] for key in MAP_VOICE}) 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), - } + +def validate_lang(value): + """Validate chosen gender or language.""" + lang = value[CONF_LANG] + gender = value.get(CONF_GENDER) + + if gender is None: + gender = value[CONF_GENDER] = next( + (chk_gender for chk_lang, chk_gender in MAP_VOICE if chk_lang == lang), None + ) + + if (lang, gender) not in MAP_VOICE: + raise vol.Invalid("Unsupported language and gender specified.") + + return value + + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LANG, default=DEFAULT_LANG): str, + vol.Optional(CONF_GENDER): str, + } + ), + validate_lang, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c39bb3cc9a4..6bd03e8c36d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==3.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.37.2 +hass-nabucasa==0.38.0 home-assistant-frontend==20201111.1 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' diff --git a/requirements_all.txt b/requirements_all.txt index a42c3829d0a..81b5d8aff84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.37.2 +hass-nabucasa==0.38.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba665b3b709..afc4428e91d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ ha-ffmpeg==2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.37.2 +hass-nabucasa==0.38.0 # homeassistant.components.tasmota hatasmota==0.0.30 diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py new file mode 100644 index 00000000000..32a4ca7cb50 --- /dev/null +++ b/tests/components/cloud/test_tts.py @@ -0,0 +1,15 @@ +"""Tests for cloud tts.""" +from homeassistant.components.cloud import tts + + +def test_schema(): + """Test schema.""" + assert "nl-NL" in tts.SUPPORT_LANGUAGES + + processed = tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) + assert processed["gender"] == "female" + + # Should not raise + processed = tts.PLATFORM_SCHEMA( + {"platform": "cloud", "language": "nl-NL", "gender": "female"} + ) From 14aba1f9de2bc1988ad1761f2cb64ee8a87261de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Nov 2020 18:02:41 +0100 Subject: [PATCH 113/430] Bump hatasmota to 0.0.31 (#43319) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/tasmota/test_binary_sensor.py | 104 +++++++++++++++--- 4 files changed, 89 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 7892b0fc231..c270be6e633 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.0.30"], + "requirements": ["hatasmota==0.0.31"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 81b5d8aff84..0b9618a8aa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.0.30 +hatasmota==0.0.31 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index afc4428e91d..b599c8bc9bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -379,7 +379,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.0.30 +hatasmota==0.0.31 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 22bf533e18e..3f444e75bdc 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -51,12 +51,12 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -64,35 +64,94 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"ON"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"OFF"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF # Test periodic state update async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Switch1":"ON"}') - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Switch1":"OFF"}') - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF # Test polled state update async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Switch1":"ON"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Switch1":"OFF"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") + assert state.state == STATE_OFF + + +async def test_controlling_state_via_mqtt_switchname(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Custom Name" + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.custom_name") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("binary_sensor.custom_name") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Test normal state update + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Custom Name":{"Action":"ON"}}' + ) + state = hass.states.get("binary_sensor.custom_name") + assert state.state == STATE_ON + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Custom Name":{"Action":"OFF"}}' + ) + state = hass.states.get("binary_sensor.custom_name") + assert state.state == STATE_OFF + + # Test periodic state update + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Custom Name":"ON"}') + state = hass.states.get("binary_sensor.custom_name") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Custom Name":"OFF"}') + state = hass.states.get("binary_sensor.custom_name") + assert state.state == STATE_OFF + + # Test polled state update + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Custom Name":"ON"}}' + ) + state = hass.states.get("binary_sensor.custom_name") + assert state.state == STATE_ON + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Custom Name":"OFF"}}' + ) + state = hass.states.get("binary_sensor.custom_name") assert state.state == STATE_OFF @@ -109,12 +168,12 @@ async def test_pushon_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota) ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -122,34 +181,34 @@ async def test_pushon_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota) async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"ON"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"OFF"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF # Test periodic state update is ignored async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Switch1":"ON"}') - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF # Test polled state update is ignored async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Switch1":"ON"}}' ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF async def test_friendly_names(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["rl"][0] = 1 config["swc"][0] = 1 config["swc"][1] = 1 + config["swn"][1] = "Beer" mac = config["mac"] async_fire_mqtt_message( @@ -197,7 +256,7 @@ async def test_off_delay(hass, mqtt_mock, setup_tasmota): hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"ON"}}' ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON assert events == ["off", "on"] @@ -205,13 +264,13 @@ async def test_off_delay(hass, mqtt_mock, setup_tasmota): hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"ON"}}' ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON assert events == ["off", "on", "on"] async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF assert events == ["off", "on", "on", "off"] @@ -222,6 +281,7 @@ async def test_availability_when_connection_lost( """Test availability after MQTT disconnection.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" await help_test_availability_when_connection_lost( hass, mqtt_client_mock, mqtt_mock, binary_sensor.DOMAIN, config ) @@ -231,6 +291,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): """Test availability.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" await help_test_availability(hass, mqtt_mock, binary_sensor.DOMAIN, config) @@ -238,6 +299,7 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): """Test availability discovery update.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" await help_test_availability_discovery_update( hass, mqtt_mock, binary_sensor.DOMAIN, config ) @@ -249,6 +311,7 @@ async def test_availability_poll_state( """Test polling after MQTT connection (re)established.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" poll_topic = "tasmota_49A3BC/cmnd/STATUS" await help_test_availability_poll_state( hass, @@ -267,6 +330,8 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog, setup_ta config2 = copy.deepcopy(DEFAULT_CONFIG) config1["swc"][0] = 1 config2["swc"][0] = 0 + config1["swn"][0] = "Test" + config2["swn"][0] = "Test" await help_test_discovery_removal( hass, mqtt_mock, caplog, binary_sensor.DOMAIN, config1, config2 @@ -279,6 +344,7 @@ async def test_discovery_update_unchanged_binary_sensor( """Test update of discovered binary_sensor.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" with patch( "homeassistant.components.tasmota.binary_sensor.TasmotaBinarySensor.discovery_update" ) as discovery_update: @@ -301,6 +367,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): """Test MQTT subscriptions are managed when entity_id is updated.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" topics = [ get_topic_stat_result(config), get_topic_tele_sensor(config), @@ -316,6 +383,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) """Test MQTT discovery update when entity_id is updated.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 + config["swn"][0] = "Test" await help_test_entity_id_update_discovery_update( hass, mqtt_mock, binary_sensor.DOMAIN, config ) From 6a5546afc1832ffa38d58b426dfe91cd1a3fe97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Wilczy=C5=84ski?= Date: Tue, 17 Nov 2020 18:24:04 +0100 Subject: [PATCH 114/430] Add an option to template delay_on/off in template binary sensor (#43259) --- .../components/template/binary_sensor.py | 38 ++- .../components/template/test_binary_sensor.py | 263 ++++++++++++++++++ 2 files changed, 291 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 0fd3205f20c..f996b91a61e 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -46,8 +46,8 @@ SENSOR_SCHEMA = vol.All( vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_DELAY_ON): cv.positive_time_period, - vol.Optional(CONF_DELAY_OFF): cv.positive_time_period, + vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_UNIQUE_ID): cv.string, } ), @@ -71,8 +71,8 @@ async def _async_create_entities(hass, config): 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) + delay_on_raw = device_config.get(CONF_DELAY_ON) + delay_off_raw = device_config.get(CONF_DELAY_OFF) unique_id = device_config.get(CONF_UNIQUE_ID) sensors.append( @@ -85,8 +85,8 @@ async def _async_create_entities(hass, config): icon_template, entity_picture_template, availability_template, - delay_on, - delay_off, + delay_on_raw, + delay_off_raw, attribute_templates, unique_id, ) @@ -115,8 +115,8 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): icon_template, entity_picture_template, availability_template, - delay_on, - delay_off, + delay_on_raw, + delay_off_raw, attribute_templates, unique_id, ): @@ -133,8 +133,10 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self._template = value_template self._state = None self._delay_cancel = None - self._delay_on = delay_on - self._delay_off = delay_off + self._delay_on = None + self._delay_on_raw = delay_on_raw + self._delay_off = None + self._delay_off_raw = delay_off_raw self._unique_id = unique_id async def async_added_to_hass(self): @@ -142,6 +144,22 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self.add_template_attribute("_state", self._template, None, self._update_state) + if self._delay_on_raw is not None: + try: + self._delay_on = cv.positive_time_period(self._delay_on_raw) + except vol.Invalid: + self.add_template_attribute( + "_delay_on", self._delay_on_raw, cv.positive_time_period + ) + + if self._delay_off_raw is not None: + try: + self._delay_off = cv.positive_time_period(self._delay_off_raw) + except vol.Invalid: + self.add_template_attribute( + "_delay_off", self._delay_off_raw, cv.positive_time_period + ) + await super().async_added_to_hass() @callback diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index a6aa253e746..e8ff4c83f8d 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -383,6 +383,269 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_template_with_templated_delay_on(hass): + """Test binary sensor template with template delay on.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', + } + }, + } + } + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + # check with time changes + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + +async def test_template_with_templated_delay_off(hass): + """Test binary sensor template with template delay off.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": 6 / 2 }) }}', + } + }, + } + } + hass.states.async_set("sensor.test_state", "on") + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + # check with time changes + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + +async def test_template_with_delay_on_based_on_input(hass): + """Test binary sensor template with template delay on based on input number.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + } + }, + } + } + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + hass.states.async_set("input_number.delay", 3) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + # set input to 4 seconds + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("input_number.delay", 4) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=4) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + +async def test_template_with_delay_off_based_on_input(hass): + """Test binary sensor template with template delay off based on input number.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + } + }, + } + } + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + hass.states.async_set("input_number.delay", 3) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + # set input to 4 seconds + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("input_number.delay", 4) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=4) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + async def test_available_without_availability_template(hass): """Ensure availability is true without an availability_template.""" config = { From 212fb572e1780093ba4de63d73fc36f1e44d214b Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 17 Nov 2020 18:27:58 +0100 Subject: [PATCH 115/430] Fix kodi media_player unavailable at start (#41714) --- homeassistant/components/kodi/__init__.py | 14 ++--- homeassistant/components/kodi/const.py | 1 - homeassistant/components/kodi/media_player.py | 56 ++++++++++++------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index f3cb15a17eb..4dcb25b3ea9 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -15,7 +15,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -23,7 +22,6 @@ from .const import ( DATA_CONNECTION, DATA_KODI, DATA_REMOVE_LISTENER, - DATA_VERSION, DOMAIN, ) @@ -48,13 +46,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.data[CONF_SSL], session=async_get_clientsession(hass), ) + + kodi = Kodi(conn) + try: await conn.connect() - kodi = Kodi(conn) - await kodi.ping() - raw_version = (await kodi.get_application_properties(["version"]))["version"] - except CannotConnectError as error: - raise ConfigEntryNotReady from error + except CannotConnectError: + pass except InvalidAuthError as error: _LOGGER.error( "Login to %s failed: [%s]", @@ -68,12 +66,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) - version = f"{raw_version['major']}.{raw_version['minor']}" hass.data[DOMAIN][entry.entry_id] = { DATA_CONNECTION: conn, DATA_KODI: kodi, DATA_REMOVE_LISTENER: remove_stop_listener, - DATA_VERSION: version, } for component in PLATFORMS: diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py index 26677f99e5e..8f0ae5de737 100644 --- a/homeassistant/components/kodi/const.py +++ b/homeassistant/components/kodi/const.py @@ -6,7 +6,6 @@ CONF_WS_PORT = "ws_port" DATA_CONNECTION = "connection" DATA_KODI = "kodi" DATA_REMOVE_LISTENER = "remove_listener" -DATA_VERSION = "version" DEFAULT_PORT = 8080 DEFAULT_SSL = False diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 68809559cbf..dfe3af4b11e 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -48,13 +48,18 @@ from homeassistant.const import ( CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, + EVENT_HOMEASSISTANT_STARTED, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.core import CoreState, callback +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + entity_platform, +) from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util @@ -63,7 +68,6 @@ from .const import ( CONF_WS_PORT, DATA_CONNECTION, DATA_KODI, - DATA_VERSION, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_TIMEOUT, @@ -91,7 +95,7 @@ DEPRECATED_TURN_OFF_ACTIONS = { "shutdown": "System.Shutdown", } -WEBSOCKET_WATCHDOG_INTERVAL = timedelta(minutes=3) +WEBSOCKET_WATCHDOG_INTERVAL = timedelta(seconds=10) # https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h MEDIA_TYPES = { @@ -229,14 +233,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): data = hass.data[DOMAIN][config_entry.entry_id] connection = data[DATA_CONNECTION] - version = data[DATA_VERSION] kodi = data[DATA_KODI] name = config_entry.data[CONF_NAME] uid = config_entry.unique_id if uid is None: uid = config_entry.entry_id - entity = KodiEntity(connection, kodi, name, uid, version) + entity = KodiEntity(connection, kodi, name, uid) async_add_entities([entity]) @@ -264,13 +267,12 @@ def cmd(func): class KodiEntity(MediaPlayerEntity): """Representation of a XBMC/Kodi device.""" - def __init__(self, connection, kodi, name, uid, version): + def __init__(self, connection, kodi, name, uid): """Initialize the Kodi entity.""" self._connection = connection self._kodi = kodi self._name = name self._unique_id = uid - self._version = version self._players = None self._properties = {} self._item = {} @@ -347,7 +349,6 @@ class KodiEntity(MediaPlayerEntity): "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, "manufacturer": "Kodi", - "sw_version": self._version, } @property @@ -370,27 +371,43 @@ class KodiEntity(MediaPlayerEntity): return if self._connection.connected: - self._on_ws_connected() + await self._on_ws_connected() - self.async_on_remove( - async_track_time_interval( - self.hass, - self._async_connect_websocket_if_disconnected, - WEBSOCKET_WATCHDOG_INTERVAL, + async def start_watchdog(event=None): + """Start websocket watchdog.""" + await self._async_connect_websocket_if_disconnected() + self.async_on_remove( + async_track_time_interval( + self.hass, + self._async_connect_websocket_if_disconnected, + WEBSOCKET_WATCHDOG_INTERVAL, + ) ) - ) - @callback - def _on_ws_connected(self): + # If Home Assistant is already in a running state, start the watchdog + # immediately, else trigger it after Home Assistant has finished starting. + if self.hass.state == CoreState.running: + await start_watchdog() + else: + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_watchdog) + + async def _on_ws_connected(self): """Call after ws is connected.""" self._register_ws_callbacks() + + version = (await self._kodi.get_application_properties(["version"]))["version"] + sw_version = f"{version['major']}.{version['minor']}" + dev_reg = await device_registry.async_get_registry(self.hass) + device = dev_reg.async_get_device({(DOMAIN, self.unique_id)}, []) + dev_reg.async_update_device(device.id, sw_version=sw_version) + self.async_schedule_update_ha_state(True) async def _async_ws_connect(self): """Connect to Kodi via websocket protocol.""" try: await self._connection.connect() - self._on_ws_connected() + await self._on_ws_connected() except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): _LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True) await self._clear_connection(False) @@ -426,6 +443,7 @@ class KodiEntity(MediaPlayerEntity): self._connection.server.System.OnRestart = self.async_on_quit self._connection.server.System.OnSleep = self.async_on_quit + @cmd async def async_update(self): """Retrieve latest state.""" if not self._connection.connected: From 1567fadda3e7b64326d673cd937743f1b923ab00 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 17 Nov 2020 21:55:46 +0100 Subject: [PATCH 116/430] Improve Google Assistant cover assumed state handling (#43255) * Set command only cover * No need for override position now that we support command only * Return empty state response for assumed state Fixes #43178 Reverts: #23498 Co-authored-by: Paulus Schoutsen --- .../components/google_assistant/trait.py | 33 +++++++------------ .../components/google_assistant/test_trait.py | 10 +++--- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 653324758e0..55c26b9499d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1521,8 +1521,6 @@ class OpenCloseTrait(_Trait): name = TRAIT_OPENCLOSE commands = [COMMAND_OPENCLOSE] - override_position = None - @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" @@ -1548,6 +1546,10 @@ class OpenCloseTrait(_Trait): if self.state.domain == binary_sensor.DOMAIN: response["queryOnlyOpenClose"] = True response["discreteOnlyOpenClose"] = True + + if self.state.attributes.get(ATTR_ASSUMED_STATE): + response["commandOnlyOpenClose"] = True + return response def query_attributes(self): @@ -1555,25 +1557,20 @@ class OpenCloseTrait(_Trait): domain = self.state.domain response = {} - if self.override_position is not None: - response["openPercent"] = self.override_position - - elif domain == cover.DOMAIN: - # When it's an assumed state, we will return that querying state - # is not supported. - if self.state.attributes.get(ATTR_ASSUMED_STATE): - raise SmartHomeError( - ERR_NOT_SUPPORTED, "Querying state is not supported" - ) + # When it's an assumed state, we will return empty state + # This shouldn't happen because we set `commandOnlyOpenClose` + # but Google still queries. Erroring here will cause device + # to show up offline. + if self.state.attributes.get(ATTR_ASSUMED_STATE): + return response + if domain == cover.DOMAIN: if self.state.state == STATE_UNKNOWN: raise SmartHomeError( ERR_NOT_SUPPORTED, "Querying state is not supported" ) - position = self.override_position or self.state.attributes.get( - cover.ATTR_CURRENT_POSITION - ) + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) if position is not None: response["openPercent"] = position @@ -1626,12 +1623,6 @@ class OpenCloseTrait(_Trait): cover.DOMAIN, service, svc_params, blocking=True, context=data.context ) - if ( - self.state.attributes.get(ATTR_ASSUMED_STATE) - or self.state.state == STATE_UNKNOWN - ): - self.override_position = params["openPercent"] - @register_trait class VolumeTrait(_Trait): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d1f5f9a1293..a579eebea04 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1886,7 +1886,8 @@ async def test_openclose_cover_unknown_state(hass): assert len(calls) == 1 assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} - assert trt.query_attributes() == {"openPercent": 100} + with pytest.raises(helpers.SmartHomeError): + trt.query_attributes() async def test_openclose_cover_assumed_state(hass): @@ -1909,18 +1910,15 @@ async def test_openclose_cover_assumed_state(hass): BASIC_CONFIG, ) - assert trt.sync_attributes() == {} + assert trt.sync_attributes() == {"commandOnlyOpenClose": True} - with pytest.raises(helpers.SmartHomeError): - trt.query_attributes() + assert trt.query_attributes() == {} calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 40}, {}) assert len(calls) == 1 assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 40} - assert trt.query_attributes() == {"openPercent": 40} - async def test_openclose_cover_no_position(hass): """Test OpenClose trait support for cover domain.""" From 682417d2c7524bd77b8c216493eec5dcfb001cb8 Mon Sep 17 00:00:00 2001 From: Alex-Klein <57624523+Alex-Klein@users.noreply.github.com> Date: Wed, 18 Nov 2020 00:20:59 +0100 Subject: [PATCH 117/430] Add TV channel name to smartthings integration (#41729) * Adds TV channel name to smartthings integration * fix on attribute to not override key --- homeassistant/components/smartthings/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index e07424520d2..835c4168f07 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -235,7 +235,10 @@ CAPABILITY_TO_SENSORS = { ) ], Capability.three_axis: [], - Capability.tv_channel: [Map(Attribute.tv_channel, "Tv Channel", None, None)], + Capability.tv_channel: [ + Map(Attribute.tv_channel, "Tv Channel", None, None), + Map(Attribute.tv_channel_name, "Tv Channel Name", None, None), + ], Capability.tvoc_measurement: [ Map( Attribute.tvoc_level, From 262fadd9bc90b89479ad353ce021a77e4cf15a6d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 18 Nov 2020 00:09:55 +0000 Subject: [PATCH 118/430] [ci skip] Translation update --- .../accuweather/translations/es-419.json | 23 ++++++++++++ .../advantage_air/translations/es-419.json | 9 +++++ .../advantage_air/translations/sl.json | 9 +++++ .../alarmdecoder/translations/es-419.json | 35 +++++++++++++++++++ .../alarmdecoder/translations/sl.json | 7 ++++ .../components/aurora/translations/pt.json | 13 +++++++ .../components/aurora/translations/sl.json | 11 ++++++ .../binary_sensor/translations/sl.json | 14 ++++++++ .../components/blebox/translations/sl.json | 1 + .../components/blink/translations/sl.json | 9 +++++ .../components/cloud/translations/sl.json | 9 +++++ .../cloudflare/translations/sl.json | 12 +++++++ .../components/denonavr/translations/et.json | 2 +- .../components/denonavr/translations/nl.json | 2 +- .../flick_electric/translations/sl.json | 14 ++++++++ .../components/goalzero/translations/sl.json | 14 ++++++++ .../components/hassio/translations/pt.json | 11 ++++++ .../components/hassio/translations/sl.json | 15 ++++++++ .../homeassistant/translations/pt.json | 1 + .../homeassistant/translations/sl.json | 16 +++++++++ .../components/homekit/translations/sl.json | 5 +-- .../components/isy994/translations/sl.json | 14 ++++++++ .../components/juicenet/translations/sl.json | 7 ++++ .../components/lovelace/translations/pt.json | 3 +- .../components/lovelace/translations/sl.json | 9 +++++ .../components/nzbget/translations/sl.json | 11 ++++++ .../components/onewire/translations/sl.json | 12 +++++++ .../openweathermap/translations/sl.json | 20 +++++++++++ .../components/ozw/translations/sl.json | 5 +++ .../recollect_waste/translations/pt.json | 11 ++++++ .../recollect_waste/translations/sl.json | 14 ++++++++ .../ruckus_unleashed/translations/sl.json | 17 +++++++++ .../components/shelly/translations/sl.json | 9 +++++ .../components/tuya/translations/sl.json | 8 +++++ .../components/upb/translations/sl.json | 7 ++++ .../components/vizio/translations/ca.json | 1 + .../components/vizio/translations/cs.json | 1 + .../components/vizio/translations/en.json | 1 + .../components/vizio/translations/et.json | 1 + .../components/vizio/translations/no.json | 1 + .../vizio/translations/zh-Hant.json | 1 + .../components/wled/translations/et.json | 2 +- .../zoneminder/translations/sl.json | 11 ++++++ 43 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/es-419.json create mode 100644 homeassistant/components/advantage_air/translations/es-419.json create mode 100644 homeassistant/components/advantage_air/translations/sl.json create mode 100644 homeassistant/components/alarmdecoder/translations/es-419.json create mode 100644 homeassistant/components/alarmdecoder/translations/sl.json create mode 100644 homeassistant/components/aurora/translations/pt.json create mode 100644 homeassistant/components/aurora/translations/sl.json create mode 100644 homeassistant/components/blink/translations/sl.json create mode 100644 homeassistant/components/cloud/translations/sl.json create mode 100644 homeassistant/components/cloudflare/translations/sl.json create mode 100644 homeassistant/components/flick_electric/translations/sl.json create mode 100644 homeassistant/components/goalzero/translations/sl.json create mode 100644 homeassistant/components/homeassistant/translations/sl.json create mode 100644 homeassistant/components/isy994/translations/sl.json create mode 100644 homeassistant/components/juicenet/translations/sl.json create mode 100644 homeassistant/components/lovelace/translations/sl.json create mode 100644 homeassistant/components/nzbget/translations/sl.json create mode 100644 homeassistant/components/onewire/translations/sl.json create mode 100644 homeassistant/components/openweathermap/translations/sl.json create mode 100644 homeassistant/components/recollect_waste/translations/pt.json create mode 100644 homeassistant/components/recollect_waste/translations/sl.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/sl.json create mode 100644 homeassistant/components/shelly/translations/sl.json create mode 100644 homeassistant/components/tuya/translations/sl.json create mode 100644 homeassistant/components/upb/translations/sl.json create mode 100644 homeassistant/components/zoneminder/translations/sl.json diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json new file mode 100644 index 00000000000..5af58867ebf --- /dev/null +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API." + }, + "step": { + "user": { + "description": "Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de entidades despu\u00e9s de la configuraci\u00f3n de integraci\u00f3n. La previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puede habilitarlo en las opciones de integraci\u00f3n.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Pron\u00f3stico del tiempo" + }, + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/es-419.json b/homeassistant/components/advantage_air/translations/es-419.json new file mode 100644 index 00000000000..f2f9a463527 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Conectar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/sl.json b/homeassistant/components/advantage_air/translations/sl.json new file mode 100644 index 00000000000..3e080b3db31 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Pove\u017eite se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/es-419.json b/homeassistant/components/alarmdecoder/translations/es-419.json new file mode 100644 index 00000000000..39344beb289 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/es-419.json @@ -0,0 +1,35 @@ +{ + "config": { + "create_entry": { + "default": "Conectado con \u00e9xito a AlarmDecoder." + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Tasa de baudios del dispositivo", + "device_path": "Ruta del dispositivo" + }, + "title": "Configurar los ajustes de conexi\u00f3n" + }, + "user": { + "data": { + "protocol": "Protocolo" + } + } + } + }, + "options": { + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modo nocturno alternativo" + } + }, + "init": { + "data": { + "edit_select": "Editar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/sl.json b/homeassistant/components/alarmdecoder/translations/sl.json new file mode 100644 index 00000000000..73dfc60865e --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee nastavljena" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/pt.json b/homeassistant/components/aurora/translations/pt.json new file mode 100644 index 00000000000..aad75b3bed0 --- /dev/null +++ b/homeassistant/components/aurora/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/sl.json b/homeassistant/components/aurora/translations/sl.json new file mode 100644 index 00000000000..d4e640e4069 --- /dev/null +++ b/homeassistant/components/aurora/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant/components/binary_sensor/translations/sl.json index a340b62ac99..02c4eedeba9 100644 --- a/homeassistant/components/binary_sensor/translations/sl.json +++ b/homeassistant/components/binary_sensor/translations/sl.json @@ -98,6 +98,9 @@ "off": "Normalno", "on": "Nizko" }, + "battery_charging": { + "off": "Se ne polni" + }, "cold": { "off": "Normalno", "on": "Hladno" @@ -122,6 +125,10 @@ "off": "Normalno", "on": "Vro\u010de" }, + "light": { + "off": "Ni lu\u010di", + "on": "Zaznana svetloba" + }, "lock": { "off": "Zaklenjeno", "on": "Odklenjeno" @@ -134,6 +141,9 @@ "off": "\u010cisto", "on": "Zaznano" }, + "moving": { + "on": "Premikanje" + }, "occupancy": { "off": "\u010cisto", "on": "Zaznano" @@ -142,6 +152,10 @@ "off": "Zaprto", "on": "Odprto" }, + "plug": { + "off": "Odklopljeno", + "on": "Priklopljeno" + }, "presence": { "off": "Odsoten", "on": "Doma" diff --git a/homeassistant/components/blebox/translations/sl.json b/homeassistant/components/blebox/translations/sl.json index f34d8d57a18..194f4b29f96 100644 --- a/homeassistant/components/blebox/translations/sl.json +++ b/homeassistant/components/blebox/translations/sl.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "IP naslov", "port": "Vrata" }, "description": "Nastavite svoj BleBox za integracijo s Home Assistant.", diff --git a/homeassistant/components/blink/translations/sl.json b/homeassistant/components/blink/translations/sl.json new file mode 100644 index 00000000000..118bd7b6a61 --- /dev/null +++ b/homeassistant/components/blink/translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "2fa": { + "description": "Vnesite pin, poslan na va\u0161 e-po\u0161tni naslov" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/sl.json b/homeassistant/components/cloud/translations/sl.json new file mode 100644 index 00000000000..a87094324f9 --- /dev/null +++ b/homeassistant/components/cloud/translations/sl.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa omogo\u010dena", + "google_enabled": "Google omogo\u010den", + "logged_in": "Prijavljen kot" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/sl.json b/homeassistant/components/cloudflare/translations/sl.json new file mode 100644 index 00000000000..02fcd2e18dc --- /dev/null +++ b/homeassistant/components/cloudflare/translations/sl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "API \u017eeton" + }, + "title": "Pove\u017eite se z Cloudflare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json index 9f793f03023..45869680bda 100644 --- a/homeassistant/components/denonavr/translations/et.json +++ b/homeassistant/components/denonavr/translations/et.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Denon AVR Network Receiver'i avastamine nurjus" }, - "flow_title": "Denon AVR v\u00f5rguvastuv\u00f5tja: {nimi}", + "flow_title": "Denon AVR v\u00f5rguvastuv\u00f5tja: {name}", "step": { "confirm": { "description": "Palun kinnita vastuv\u00f5tja lisamine", diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index eac5783809c..9f79aebeb60 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd" }, - "flow_title": "Denon AVR Network Receiver: {naam}", + "flow_title": "Denon AVR Network Receiver: {name}", "step": { "confirm": { "title": "Denon AVR Network Receivers" diff --git a/homeassistant/components/flick_electric/translations/sl.json b/homeassistant/components/flick_electric/translations/sl.json new file mode 100644 index 00000000000..562dfd70bc6 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "Geslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/sl.json b/homeassistant/components/goalzero/translations/sl.json new file mode 100644 index 00000000000..7dcc7b43684 --- /dev/null +++ b/homeassistant/components/goalzero/translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "name": "Ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json index 981cb51c83a..973601e744a 100644 --- a/homeassistant/components/hassio/translations/pt.json +++ b/homeassistant/components/hassio/translations/pt.json @@ -1,3 +1,14 @@ { + "system_health": { + "info": { + "disk_total": "Disco Total", + "disk_used": "Disco Usado", + "docker_version": "Vers\u00e3o Docker", + "host_os": "Sistema operativo anfitri\u00e3o", + "supervisor_api": "API do Supervisor", + "supervisor_version": "Vers\u00e3o do Supervisor", + "supported": "Suportado" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sl.json b/homeassistant/components/hassio/translations/sl.json index 981cb51c83a..eb2f5f7ca8b 100644 --- a/homeassistant/components/hassio/translations/sl.json +++ b/homeassistant/components/hassio/translations/sl.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "disk_total": "Skupaj na disku", + "disk_used": "Uporabljen disk", + "docker_version": "Razli\u010dica Dockerja", + "healthy": "Zdravo", + "host_os": "Gostiteljski operacijski sistem", + "installed_addons": "Name\u0161\u010deni dodatki", + "supervisor_api": "API nadzornika", + "supervisor_version": "Razli\u010dica nadzornika", + "supported": "Podprto", + "update_channel": "Posodobi kanal", + "version_api": "API razli\u010dica" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index 1b30839faff..7bf340567f5 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -5,6 +5,7 @@ "dev": "Desenvolvimento", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", "host_os": "Sistema Operativo do Home Assistant", "installation_type": "Tipo de Instala\u00e7\u00e3o", "os_name": "Nome do Sistema Operativo", diff --git a/homeassistant/components/homeassistant/translations/sl.json b/homeassistant/components/homeassistant/translations/sl.json new file mode 100644 index 00000000000..64e972f5a44 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/sl.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "arch": "Arhitektura CPU", + "dev": "Razvoj", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Nadzornik", + "installation_type": "Vrsta namestitve", + "os_version": "Razli\u010dica operacijskega sistema", + "python_version": "Razli\u010dica Pythona", + "timezone": "\u010casovni pas", + "version": "Razli\u010dica" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json index af2823da6d4..caeba3a9b6c 100644 --- a/homeassistant/components/homekit/translations/sl.json +++ b/homeassistant/components/homekit/translations/sl.json @@ -6,7 +6,7 @@ "step": { "pairing": { "description": "Takoj, ko je most {name} pripravljen, bo zdru\u017eevanje na voljo v \"Obvestilih\" kot \"Nastavitev HomeKit mostu\".", - "title": "Upari HomeKit Most" + "title": "Seznani HomeKit" }, "user": { "data": { @@ -36,7 +36,8 @@ }, "include_exclude": { "data": { - "entities": "Entitete" + "entities": "Entitete", + "mode": "Na\u010din" }, "title": "Izberite entitete, ki jih \u017eelite izpostaviti" }, diff --git a/homeassistant/components/isy994/translations/sl.json b/homeassistant/components/isy994/translations/sl.json new file mode 100644 index 00000000000..d241fbeb59c --- /dev/null +++ b/homeassistant/components/isy994/translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "host": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/sl.json b/homeassistant/components/juicenet/translations/sl.json new file mode 100644 index 00000000000..8a0996ed92e --- /dev/null +++ b/homeassistant/components/juicenet/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Nepri\u010dakovana napaka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/pt.json b/homeassistant/components/lovelace/translations/pt.json index 920f9447577..dd8cc7cc32d 100644 --- a/homeassistant/components/lovelace/translations/pt.json +++ b/homeassistant/components/lovelace/translations/pt.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Dashboards", "mode": "Modo", - "resources": "Recursos" + "resources": "Recursos", + "views": "Visualiza\u00e7\u00f5es" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/sl.json b/homeassistant/components/lovelace/translations/sl.json new file mode 100644 index 00000000000..4042b5a8d4c --- /dev/null +++ b/homeassistant/components/lovelace/translations/sl.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "dashboards": "Nadzorne plo\u0161\u010de", + "mode": "Na\u010din", + "resources": "Viri" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/sl.json b/homeassistant/components/nzbget/translations/sl.json new file mode 100644 index 00000000000..d4e640e4069 --- /dev/null +++ b/homeassistant/components/nzbget/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/sl.json b/homeassistant/components/onewire/translations/sl.json new file mode 100644 index 00000000000..7011c57c099 --- /dev/null +++ b/homeassistant/components/onewire/translations/sl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "type": "Vrsta povezave" + }, + "title": "Nastavite 1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/sl.json b/homeassistant/components/openweathermap/translations/sl.json new file mode 100644 index 00000000000..76fcbca199d --- /dev/null +++ b/homeassistant/components/openweathermap/translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mode": "Na\u010din" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Na\u010din" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json index 03b6d975c2d..c4d67feb5bf 100644 --- a/homeassistant/components/ozw/translations/sl.json +++ b/homeassistant/components/ozw/translations/sl.json @@ -2,6 +2,11 @@ "config": { "abort": { "mqtt_required": "Integracija MQTT ni nastavljena" + }, + "step": { + "on_supervisor": { + "title": "Izberite na\u010din povezave" + } } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/pt.json b/homeassistant/components/recollect_waste/translations/pt.json new file mode 100644 index 00000000000..57e7ea502f5 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "service_id": "ID do servi\u00e7o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/sl.json b/homeassistant/components/recollect_waste/translations/sl.json new file mode 100644 index 00000000000..cae09d77621 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "step": { + "user": { + "data": { + "service_id": "ID storitve" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/sl.json b/homeassistant/components/ruckus_unleashed/translations/sl.json new file mode 100644 index 00000000000..6e82b8dd9a3 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/sl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sl.json b/homeassistant/components/shelly/translations/sl.json new file mode 100644 index 00000000000..8d0b45b44e1 --- /dev/null +++ b/homeassistant/components/shelly/translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Pred nastavitvijo je treba naprave, ki delujejo na baterije, prebuditi s pritiskom na gumb na napravi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json new file mode 100644 index 00000000000..4879603af6c --- /dev/null +++ b/homeassistant/components/tuya/translations/sl.json @@ -0,0 +1,8 @@ +{ + "options": { + "error": { + "dev_not_config": "Vrsta naprave ni nastavljiva", + "dev_not_found": "Naprave ni mogo\u010de najti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/sl.json b/homeassistant/components/upb/translations/sl.json new file mode 100644 index 00000000000..8a0996ed92e --- /dev/null +++ b/homeassistant/components/upb/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Nepri\u010dakovana napaka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json index 02bfb857d76..d18ffb044c1 100644 --- a/homeassistant/components/vizio/translations/ca.json +++ b/homeassistant/components/vizio/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom, les aplicacions i/o les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, l'entrada de configuraci\u00f3 s'ha actualitzat." }, "error": { diff --git a/homeassistant/components/vizio/translations/cs.json b/homeassistant/components/vizio/translations/cs.json index 23fec08499b..cf0c66749dd 100644 --- a/homeassistant/components/vizio/translations/cs.json +++ b/homeassistant/components/vizio/translations/cs.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "updated_entry": "Tato polo\u017eka ji\u017e byla nastavena, ale jm\u00e9no, aplikace nebo mo\u017enosti definovan\u00e9 v konfiguraci neodpov\u00eddaj\u00ed d\u0159\u00edve importovan\u00e9 konfiguraci, tak\u017ee polo\u017eka konfigurace byla odpov\u00eddaj\u00edc\u00edm zp\u016fsobem aktualizov\u00e1na." }, "error": { diff --git a/homeassistant/components/vizio/translations/en.json b/homeassistant/components/vizio/translations/en.json index 41dc47150f9..55176b962f2 100644 --- a/homeassistant/components/vizio/translations/en.json +++ b/homeassistant/components/vizio/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Device is already configured", + "cannot_connect": "Failed to connect", "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." }, "error": { diff --git a/homeassistant/components/vizio/translations/et.json b/homeassistant/components/vizio/translations/et.json index c4db3bdce26..ab88700d173 100644 --- a/homeassistant/components/vizio/translations/et.json +++ b/homeassistant/components/vizio/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", "updated_entry": "See kirje on juba seadistatud, kuid konfiguratsioonis m\u00e4\u00e4ratletud nimi, rakendused ja / v\u00f5i suvandid ei \u00fchti varem imporditud konfiguratsiooniga, seega on konfiguratsioonikirjet vastavalt v\u00e4rskendatud." }, "error": { diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index 3836bc784d5..c5e0b6386b8 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", "updated_entry": "Dette innlegget har allerede v\u00e6rt oppsett, men navnet, apps, og/eller alternativer som er definert i konfigurasjon som ikke stemmer med det som tidligere er importert konfigurasjon, s\u00e5 konfigurasjonen innlegget har blitt oppdatert i henhold til dette." }, "error": { diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index af72d1e964c..74d6a858d84 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "updated_entry": "\u6b64\u5be6\u9ad4\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u5be6\u9ad4\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json index e32427a1b7d..9fb057a43d8 100644 --- a/homeassistant/components/wled/translations/et.json +++ b/homeassistant/components/wled/translations/et.json @@ -16,7 +16,7 @@ "description": "Seadista WLED-i sidumine Home Assistantiga." }, "zeroconf_confirm": { - "description": "Kas soovid lisada WLED {nimi} Home Assistanti?", + "description": "Kas soovid lisada WLED {name} Home Assistanti?", "title": "Leitud WLED seade" } } diff --git a/homeassistant/components/zoneminder/translations/sl.json b/homeassistant/components/zoneminder/translations/sl.json new file mode 100644 index 00000000000..f05e5d6d329 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "verify_ssl": "Preveri SSL certifikat" + } + } + } + } +} \ No newline at end of file From de5e49b37c7d29af4f96de88adbd773aae17ce19 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Nov 2020 08:24:25 +0100 Subject: [PATCH 119/430] Updated frontend to 20201111.2 (#43334) --- 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 893683dfb88..aea1717ce6f 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201111.1"], + "requirements": ["home-assistant-frontend==20201111.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bd03e8c36d..9465ff15124 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201111.1 +home-assistant-frontend==20201111.2 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0b9618a8aa3..9af0e884fb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201111.1 +home-assistant-frontend==20201111.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b599c8bc9bd..f05ffa82c8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -397,7 +397,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201111.1 +home-assistant-frontend==20201111.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From b25e8be3f2bae4698b6547bd7d990b67096edc2d Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 18 Nov 2020 07:18:59 -0700 Subject: [PATCH 120/430] Tweak OZW Light discovery (#43325) --- homeassistant/components/ozw/discovery.py | 2 + tests/fixtures/ozw/light_network_dump.csv | 93 +++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py index a83f763c810..67e3442cf5f 100644 --- a/homeassistant/components/ozw/discovery.py +++ b/homeassistant/components/ozw/discovery.py @@ -254,11 +254,13 @@ DISCOVERY_SCHEMAS = ( "min_kelvin": { const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,), const.DISC_INDEX: 81, # PR for upstream to add SWITCH_COLOR_CT_WARM + const.DISC_TYPE: ValueType.INT, const.DISC_OPTIONAL: True, }, "max_kelvin": { const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,), const.DISC_INDEX: 82, # PR for upstream to add SWITCH_COLOR_CT_COLD + const.DISC_TYPE: ValueType.INT, const.DISC_OPTIONAL: True, }, }, diff --git a/tests/fixtures/ozw/light_network_dump.csv b/tests/fixtures/ozw/light_network_dump.csv index 2f0cc7019dc..e9c0d8fb74b 100644 --- a/tests/fixtures/ozw/light_network_dump.csv +++ b/tests/fixtures/ozw/light_network_dump.csv @@ -225,3 +225,96 @@ OpenZWave/1/node/2/instance/1/commandclass/134/,{ "Instance": 1, "CommandC OpenZWave/1/node/2/instance/1/commandclass/134/value/48332823/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 2, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 48332823, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} OpenZWave/1/node/2/instance/1/commandclass/134/value/281475025043479/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 2, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475025043479, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} OpenZWave/1/node/2/instance/1/commandclass/134/value/562950001754135/,{ "Label": "Application Version", "Value": "3.37", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 2, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950001754135, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} +OpenZWave/1/node/12/,{ "NodeID": 12, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:0063:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw099.png", "Description": "Aeotec Smart Dimmer 6 is a low-cost Z-Wave Dimmer plug-in module specifically used to enable Z-Wave command and control (on/off/dim) of any plug-in tool. It can report immediate wattage consumption or kWh energy usage over a period of time. In the event of power failure, non-volatile memory retains all programmed information relating to the unit’s operating status. Its surface has a Smart RGB LED, which can be used for indicating the output load status or strength of the wireless signal. You can configure its indication colour according to your favour. The Smart Dimmer 6 is also a security Z-Wave device and supports Over The Air (OTA) feature for the products firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2246/Aeon Labs Smart Dimmer 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action button that you can find on the product's housing.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action button that you can find on the product's housing.", "ResetHelp": "Press and hold the Action button that you can find on the product's housing for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Dimmer 6", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACiCAIAAACLVRX1AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO1dWW8cx/Hv2fviklzepCRSsnzJVnzKARLkwfn7IQZywDmA5AskyFM+Q14C5BvkEwRIkDiJkYcYCiAHsOU4Vqw4tuzIpiRSssSbS3K55+zO/B/KbDWrqmt6l4cYh/VAzvZUV1d3V/2qpmemxwuCQO0TeZ4XhuF+SdsvOmStbM25q/HAh3FfFEhoQfqvUoo90K3KOpm19LEgWRMt0e2a5UgCZWArUk62OUoyGxofk5OtZRsKZQwdYutKK7ncNuaoXCgUmkCnvENArMN0wd7aOgQND7qJB45ziD7XRsYV21nlADOCDykL0lBAYjmVHbRsWgkq2RxUUFUglyZkrWwYo/UBcteK7UW3AURmMJvYT8TqmfbF20wHOFAFZGbT0E2vcJGP2JCoo4ZJMvGIBeQSkmWXomxyYkSJbVGRsXaBh8isxRTLnnKXJjfhyEwNC52SVbJNU+T0RZ6KbEIBYj1AV+jKF13YbE4vM+/jCDgqILR4lJHJXbf7huWOWOyBI5uQOrDUm0vRLARFpb0kTO4er3ZjtnmWzVaVBc/cI4lNgiDNRfkeRB2JHOuB0yGARA9NHGXoiqSIdSygvSdGtDDSC+XybnnkJlRUCtWVerY+dguT3VaRpXU7vHuZEXU0EauHvMTGY8ui0CWb2p28d9Xi3hVwl3Y4tC+N7hJhc1MhMXK0626v0Uw2FZV5CFemSAebKFkBl0uwwxR1cDCzF1HMVWFX7R1xOrJ5yX8v9vRGOMdSDqvhkUK7wjZ9lm3apVFUNzJl7Ir25QIW0d61ctchcuiEWMRW7yLHOoJeTlU6UCX3KBxlbFQUWvLoTSvk/zKzTZMDIqb1UFx5VxwAmORu7D1fFVJRPfAITbgw95xKUmbbICMJB4Fn3SrWVS7I5Fg9m3a3FffiQ/vuf5Ews3exe+HpVqsD7YIjHO4a0i9Y8n5MR4RiKiqICGf3F6s9zwOBNrFmeu6uj1OmuYeOIK0ie7HH5v5b6PNQCD/kdSy2frcXgHu/wESZn8zjqNXhUFcLaZGrdPu1XhWpQG9NHLnrwWP6YlAC/vVs6T2vz+5lScyFZ1/WymnFMAxbrVatVvN9v9PpgFvGYrFkMplMJlOpVDKZjMfj3WrbM9shCOlN/jFiRVAQBPV6fXt7u9lsNpvNVqsVhmEicd8hTQqCIAxDbWfa2lKpVDqdTqVS/wvZFZCUY2mSsxmXdayel2cOIWlAPEEQ1Gq1arXaarUajYbv+57nmZZkMrMHYGTKMLsgCADeEokEoBoYXDqdTqfTmUwG5LsviR1E3/dexaT/dcQKw7DdbtdqtXq9DpjUbrdjsVgikaB2ozhjci9EBgfwFgSBbjFlEBhcJpOJx+P/jTgnrbxr2serQoH2PSGw6eb7frVabTQarVar2WwGQRCLxSAx6s1cuq1CCynCaYLAqq1NG1w6nY7FYgd35btHUZ8jvOMCkuOp3hTad780Q3yz2dzY2KjX661WKwiCZDIZi8WUUvF4nM21WSA/CHQ3/VmvXJtWDqbW6XQ6nQ6Ymu/74B7xeBx6EYvF0um0XkLbL8X2VN1lsHrIsbpCOMfWu5IA/Wq1Wmtra+VyWSmVyWRgGlgIUd3ADMvgzikc65/xeDyVSmWz2Ww2m8vl0um0dgDzckHXarVa8XgczMs+Kpj2d6ZM+sLmWI1GY3l5eWNjI5vNZjIZRYIOsLE/hQMXTtuxzBCLxWRLMkELWZXnebFYLBaLhWGYzWbBfx4sdfFe4V7SKQHzZGku8hFPp9NZW1tbXFzMZDJ9fX2Km0VbiqN2p9gCj+2UKcHkoWqAJaXT6Vwul81mwZL0KIU715JgTKxMevsBsq58Pt/VMHYFTo7MXyjECsOw0WisrKysra2NjIzoNQKWUz4Q/vZ2CoQnk8l0Og2whK74AIo0JoExKe7GlLIbFthrLBYrFAp7Gso9U4RhmebZG5zsnRxBKwiCjY2Nzc3NjY2NiYmJyHBg63i3Nmf7qQ/i8ThEt1wul0gkzPE0MUlbkiYW45Fhsfp7ngcXj/IIoCqOzI5kfR6LLRTOCvzyKcXNMSsfDlBuAVNYrVaXlpY8zyuXy1NTU91exiJitbVFNH1AzS6dThcKBTPp0ZakA5zc5UjrF6hYLLqbi+P0yVaxq5YeiN5yLHfaizSWGTRvNBpLS0urq6uFQmF7e3tsbIxGwMiGur2SspWYIa+vry+dTsPPTqfTbrfb7bYZ4IQIYAKSyQbp48rKyubmJtwbiMVi/f39J06cmJqaMkE6DMNUKpXL5dz7tb8UjVjobKRp04qR4KcIDinD1qlkIN/3V1ZWlpaWqtVqLpdLJpMQcZAc2pZc0i2DSaBkLpfT6XO73YaVM5RsaeHyyEB8X1hYWFpaWllZWV9fh/mCijqGdjqdgYGBl156aXp62hTY399PW0GZmdlNVpmuoPQ+j4lYtpGixyxDV+RYkXXrIAjW1tYWFha2trba7XYikRgbG6tUKqOjo5/3KsqA9m5z7NlYLKaBKggCuEGElI+UX6lUFhYWlpeXAYnb7bZ5qWhSp9PR5a1Wy/f9V1555Utf+pJuCy42hV4cHCWo8SJiUwrTZlEhDckCdJkhmA3Hajd0KaU2Njbu3btXLpdhAdrzvIGBgXK5PDo6Co9ZUwdVZEYFBpTMsdNvw/hisZhKpZRSvu83m005DujCZrMJmLS8vLy8vFyv13VQC3du7wA46TvZkLp5nuf7frlchrZardavf/3r4eHhiYkJqO77Pujj2IWeiUE1YbcZTXvMrvZFglIqCIJbt24tLi7CsysgMJVKDQwMNBqNUqmkopBJhqVuUQ3VLRaL2WxWKdVqtVqtlj5lOgb87XQ6y8vLi4uLYEmbm5uw7qChyMzxwzBMp9NjY2OTk5OTk5Ojo6OQlZu+evfu3UuXLl27dq1er58+ffpnP/sZ6BYEwcDAwIGGFBvhHCsyv6EAY6uIys26yo4QaveEmaI+/vjjpaUl1OFSqbS9vT0+Pr4Xs+iWnzLn83lIaHzfbzQailAQBHfu3Jmfn19YWFhdXYU8Se0EMg1IcJBIJEql0uTk5NTU1MTExODgYDweZ0OH1icIgj//+c+vv/56PB7/6U9/+vjjjwNbPp/Xz+TIOG2LKghl6RSjWnB8f1mFWol5yvaTHX1hkugB5WELb9y4sbi4qOcDephIJPRdZDbIohJToHtwtDFoSqVSfX19nue12+1ms4nOrqysvP/++7Ozs77vo3UHeGam0+kA4E1MTAAsjYyM0NxI1tzzvG9+85uffPLJp59+evnyZTAspVS73U4mk6zaqARNjTmALpOFRCXQBFCztSEW6iQ7GcowcFtQZ8EPcW5sbNy5c4c2kc1mNzY2hoeHhWwGEYvwNptz7GZ/f38sFguCoNFomKcqlcobb7xx69YtffPb932wpCAIstnsxMTE1NTU5OTk2NgYXEjSzFXoi6kS/P3BD37w85///Pr16/V6HZIw/fw0W9Fmo2bgRg056pbwOJtljxEkCpzU2G1IRnmozCAIrl+/DiFDF0ITqVSqVqvBpKLqrKmZdQUwR62gEqRePp9PJpNhGMKjXfrUxx9/fPHiRbieaLVacAU3ODh48uTJEydOTExMDAwM0NsDjkZv6+D09PSZM2eWl5dv3rz5xBNPQLqGsEBoToAoQUOWGV8V0jiKEIWOLGttQtxh1bKBYhiGCwsLtVqNykkkEoDz+p1bl4BrI4pSbI8QxWKxfD7fbrfj8TisLIDm//jHPy5duhSPxyHMTU1NPfHEE2fOnOnr6xNSpUjdXE49//zzr7322tzc3Llz55RS6IXkyHyADVbK8DGbxyLJCXYchfyJFRpZHjnBqEWTf35+nsXkVCq1sbExOjoKk4cqCtmDzGaL16za6XS60+kkEgm4DAS2jz766K9//Suk5I888shXv/rV8fFx4LddoNDJo/hqswak0pNPPvm73/3u3r17cApCoc1b2J+OQycPEYNY1GzpiNiiiT4W0ilagjzDPLuxsVGtVukzuJ7nAWIppVAcdB8a2anYKyazd57n6ccTINIppcrl8p/+9KdGo1EoFF5++eUnn3ySDi8dItm2TLbIksnJyVQqtbKyAqdisVin0zEfyBF6Rw/YJtiZQgIZxDLNlg1ziIdSDxHa5hALCwv6eTdh+oXkmq2Ijt0PzJ9g7rFYrN1uazt47bXX1tfXS6XSD3/4w1OnTtnGQXEjLKACNXeBc3x8HN5Xy2QykGbp+6fdIjrbBFuOquy6KqSJji4369hATvMIsGRThWVQSq2traHeakdsNBrpdBpVERI+xU0MHZHIQv0TXsGIxWLwiphSanZ29sqVK319fa+88opsVbrLtpjI9kLGMN270dHRubm57e1tfXNJQCCKTzIgsSV0ihNaBHJ91qooG1vCdj4SxilDq9Wq1+vJZJJGh3g8vrm5OTY2RuOgskyMrVwwO5tTwgE8Uqd2EmTP8/7yl78EQfDlL3/50UcflRNzeXjRTxuzqZh5qlQq3bp1q1qtDg0NRVaPnHGXEjrFzHbcbMRlfR31TTlfycsxXpeXy2W0+KltCwKQrFVX5sWWC14LbwKaKx3Ly8tXrlx56KGHvv71r7ukRzSdoGhhk2PL/+BvsVhUStXrdW/nNpEyJkiJM24bUhaiKJsmV8SSpbAV6Sm2RPDdSqVC03Ygc8h6oz2aned5EAf1CuTf//73VCr13HPPoTUFlmwTw46M4JnI5eAnPOmP7izZgozjREeGF0TM5raKs25aE0EURSy2hB7QcdHl1WoVZSG0t5GuI1PIJS7Kwbx0gqUfa7l8+fLQ0NCzzz4rVDQJ9d3WLxbYqBCTDR5Kazabios8ZossNNK5sIUUVhQQRiyqeqQJy1DneMAea59DuO15HlxCC+rtCwlmp18WBQW2trYWFhaee+65sbExF5UoAPSWRLOhLZPJeJ6nF2xNTiE+2H5GVmH7y2/HzcIMIvdszGbparfXIga143NqtzsChrVaLbiRQmUiIfI09wZ4Og7qujdu3Egmk2fPnmUFsmPCimXP2nCdVoG/8NoqGJYwjxSWhGFkWzd50FkGsWTTNpthDyi/izewDL7v68050Cg0Gg1YpDH7LDRqm4/ezM7zPHjfRp+9fv16X1/fqVOn5B6xJTZPRq27OwA8zqDXbF106GHuaKF5Fu9BSp3GluLQcpoNsKLMtMnbIcQGDoduApqczWYTPRsZhiEdBZOEUzZOgcFELM/z5ubm+vr69KObFHLYEtR386cwjJGEjB6NoSJTIDRBJ4vtHaODigqokYiFSgSrpwxC06bD0TGCHItti+2niXY2mULXEKe3czNH92hpaWliYgKe9WMluOCNCVSOU8iKBd3Q62Xm4EdOgXzWBcbuIxYbrW0/FWe/7ohlnkK+qwv1cjYSAsSui0JFPXbmQGiir6ubPLSELTcNC8B1e3sbnvOknbIhjZZsyqd2T7vD9tokWKMJyco+qmUe0ElhC+mMoL/6wDXHEpBMAKRIBnawTMRCCGTaDa0ekvSTnTNaxeSPBLxw5zV2fbZSqSil4OlyZAEm/CAypxYZge4FnWMXLATbYk/RWsKkuIAZOx1hGOKnoc0hQMgsO5ZZHooLIcLMmZz6YRjaOlVDoK6MRpagf8K0af6NjY1kMgnr3ebIUL+3kWB5NrVNNnaoFbFdVAXNNaprHlMyq+u6JnNC6LyAlpSHFlLYFBSlDNpfNQZ4DldJVL7g64o4peMUou0bNzc3k8kkLEtSIaxYAUtkJeWzyEkEwGZ/slPGki0makqY5maGEgQzphQUcdhRo8inyBzLqpuXhBppZcgReGxnZR1sM4qe5KlUKvAetuI8xN0h0fjbdGajBDrr7RAKUkg+BRsabVB1YRbMs7sQywZRtsmIhB8bW1chTBGLjGxREBUJS0gsxQPIYMzyarUKO105ilVcR9AUsu3SinSmPSNjs0V5IRax8OOCZKiidQepI0JoTKkLKgczjWSwSXO041qtBnseu8gXtGKxv9VqVSqVjY2NXC43OTnpkXRTmGl3F9p3OrqGJSQWNjxXBGNoLWUYK00DKGCwAtHbNc1mM5FICPu8sUKgpNls6p12EbPneZVK5c0332y324ODgw8//LAj3HaFygdEidASU+UkQChRlsitCBiwWR3iMcuFVIONIIJW+oLA9lePhgw22jjgYQc6mLZscnV1dX19fWNjIwzDZ555Bj5aQRWoVCrvvPPOiRMnTp06ZXsvl2qLkmDFjb85JoiHepowcVQTJseiA2cbUJnTFrkFHqEKmi1FhomFHLZRc+YcfVrooD6AV5z1paJLRvLuu+/Oz89PT0/PzMxQgagXsMGk7axNW/NRNpcJZecicuJsmuCrQpOPDS4srph9ZmfXJooKESDQlKAhx6wuePM+ElW70+noJVMEV7YewV4g8IEdxWGMlu95HuwkIOOE2j28CIcctUIybb5t08S0Aeub0IqYcCSbcjDnSNcReov6tu9phGNSrHZ7i1IK1nLZKWctXgtJpVJorZU2qp8oZM+yc0G912WObDIFIWytz0OhHGX1T7M+Wy5URAfID5B+JjjZTqESlwSO9j8yvaNN0DGFY/0yBeqsTQJQPB4397q19dHbuT+DFDN/opEPyXKDDWzkKbYNly5nDQAO9nkdy1aRrcI6ja0tNi2VAVJGQVtn5bNsqDK3bzRPsceaIHrK0cDGJhOybISm7HGkGi61zBJm5d0GM7Rc2VEHMQggZxsd2lUUVpAXmrVsP9kS2gTiZPGGqtEVeTvfkqDS0AizbKwyNlhVZEDMctsUsLgrM5g/matCG8z0hjou4NEbUYu36WAy29RghVCBdIbYwKFlIiVN0jiEEEWRATTZZM8UYpPQKZdTcvChP3lX0B6A8Jx6BosiZkUbzAgYQBW1earLxChi3IKoyLM2i9THJikOS7QcMx8P7WT2LhIdgZnN+UwNWeVtnUIlkdWhkP+sHDsTAmIJFW2IJUdxpDc7RjLZYnS3kOkYKWRmxbmNaTEor7CBqxDHqdouEcYdybqNV045FlQwy6kPmSMixGPURLlcnp2dbTabxWKxWCz29/cXCgX0XRC255SQVjK0yNVpOTvlXcmhJDQn6MyWyDMo9IKdPqQJlRYpKtS7zdC/tOfuHhBp71qV99577969e+12G/ZQhL32U6lUJpPJ5/PDw8PKbsq2JlgFbIXCWTMMCUDbVSsmmwkqqK5Zjp6lprBN5w6Zi7LPrABgMnPk2S4QS1n8ySXisAaud9XWd3BND9Nb77NpCh1fuVwmz5Ji25pA/eoBX1k5VAf4CoG2LQGuzEJ4qpsiFlsRQZHJIChp65Tm6QKxaImtgUjEggO9GbpgDQiNlWFn7q27kA1rXWqxhawvsVOoLOOvlCqVSt/97neHhobMHQPNsWLV9n1fkCm3KHTNZYg0D3Ov0PQ/c2hYfLIVCiVamrnFo1ZreHh4dXU1Ho/D6Aj9FGyxB9ByJ2FkhWDRm0qFQuGpp56C5ya2t7epHNbBtBXSqUQkIJkiHoKqyOGLX3kHskVolkcupCPu7SxYm2ydTmd+fv4b3/jGzMzMRx99dPfuXdsQmKPmqNVRID1nGr1kg4NR0t+nUMRt2J6iDZ66Qnc617JV2Nh2PWJLvcHsvC3Gsxrb2MwDs8OmE8zMzCil9Iawkc2FYVitVufn57e3t+EDO2wtVv9uqQcJ7AjbciZE3u5FKZdWzPRU7UagrpS3qcdOJfqrzDehKazJ4ugpmsMiv2H5TZ52u72wsIBGn9UKnVpbW/v973//9NNPw7PnmUwGPnyVSqVSqRRcHMA3LPTLnHTI3MllNOTCZrOJPjeHsg7W5eTmkFbowXx326KzhpoWuq8LE0qMXEKspQxydUro3ipEt4mJiWq1an7S2Bxo2hM4C09X6hDQ6XRarRY8PgD77vm+D7tOgtklk0ltZ/F4HD4fzyqJGpJ53O31pZdegjfGNjY2UHXT1IR4QtVDEmzhTIiMJj/NPVz6dT/HQko7+oTA436WPjSSz+cfffTRP/7xj9/61rdYpKUd0N6plII4CN9h832/0+lsb29vb2/DZwQHBgaSyWSn06nX641GQ6cvnU4nlUo99thjQhdsvUAZiS1BpoXT09OQMw0ODup3vln5LuFSGQsfZgJjC4LdJjCOZPLff/6fGqkJgwg26CWeo0+YvaVvHwDk5PP5X/ziF4899hj7dWcUO7TySql6vd7pdGq1WqPRiMVimUxmYGBgYmJCP4GpP/gGo6+/Fw+fnXEdP0t3bMRired5vu9D/qTfUqT9UsRYbQ5mO5CNXiDbJNLZZ/kTZgWqNypngQ35Bx1HG6TDGwSI4cMPPwzD8Ny5c+Zw23quj2GT2ffff//UqVOFQmFoaMjzPB0EzR30TbcOwxA2SzI3JHJECFMNmlxGkpkj9gYnlBmNvEdWDdzFyprYgpgrYrHiWAeyeYkpnEqLxWIDAwNra2uNRsNcLIa5Z5tg7dXbeVKg2Ww+/vjj5i7ZQCY+qZ2bJDobC4IAdipjlRcchnbN3cKoTBvks2HENqpIsi3CoCouWZc7gbRoxKLVHAtdNDh//vz58+fhg2zVarW2Q9VqtVqtKrtD0CsXtfuDRJ7xJB2FhND4imkQBPJ2pvLI2DR0IdYC2FiviH24qCREGKEXe6cQdpsBEnIss0R2aJkNlWjh8Xg8n8/n83mTYWtr6+2336Z4IIyp3p7KxmCKgqtCYIP9PHogOR/SxCrDZkVhGPq+r7+ZwLKp3X6ljLlj8cwWRnqjfc6xbD9tFSPlCIVAtpzXPDYjGoDQjRs3BgYGYB3LnB4XEnJV9wyM4opNLC3c2tr65JNP2u12sVh85JFHhFerbTqj8WFxfe+0DzkWm2HI4MSyUf7IuI78WOCBPH1qaqrdbs/Ozm5tbcGXTmF1CrAwl8uZf/V1g9AL1ATSSlAbBTi5d5pqtdpvfvObWCw2PT0dj8fv3Llz5swZti6aS9qWmafuI1xFpuAYsUzI0UqbwZ52Bkm0tSSXu/tQZGAdGhr60Y9+FARBq9VqNpu1Wm1ra6tcLsNr7Ovr6+vr63Nzc9VqtdFoQLaey+UKhQLY2dTU1LPPPouiCetLQirdW9c0Xbp0aXFxsVAozM3NPfnkk5ubmxATI0fG1BBNVm9DLTDLcnYhFkobUQCiSaVLjuVC7ohla5fyx+PxbDabzWYHBgYmJydRVIIvgTcajc3NzfUdKpfLa2trc3NzyuLcMr7aIhFNPtjcQAtvt9vvvPOO53nwXapyuTw+Pl6r1fr7+wWMt+EoRQfKI5BjSLF1Bwh/CBPZuIvt25KzyIyE7T9LXSXFNga4vZPP54eGhnSU0X3vSlSkekKizdLs7Ozm5mY2m4WPtW5tbY2Pj7MPDrGhg5XP3it0IRSsXPSnTTitYyGnYd2RTVm69RKboo6A0TPtl5zeGvU8b25urtlsLi4uNhqNc+fOTU1N6SsPilLaDWzZrX6BNjI1FAbWPOWYj5rkuo5lQzJbxT1eg+hBodH5SJHLKLvkoKurqyAHLm+TyaS5sZE7mWaHCllmQT3FIZb7XOx60I/mUiYg0XJTA0enZznDMLxx48bVq1eDIBgeHh4ZGRkaGspkMojtCFqYS69tapt16/V6u93OZDLwmV39Wj1NbSO77+08QYk2c1NceJHTKTPyuEQh0/4Skdc1jsmW+3ybnJ7n+b7/q1/96re//S18kgmeY0kkEqVS6Sc/+YncBLV7ZfEwWwRXuyfedqzcPMpxEKgEeE8pl8vBIz0uW1vR1rUC2rDQZJnJE4IxNCm0okvvTPl4n/dDpjAMX3311YsXL05OTsLg+r7farXa7XatVtM8QnZsC83oWGCLrMv+RGqwPx1FhWEISyRwE1P41kukZCB4mUAjFot2kREwshW5orTP+yFQpVK5cePGyZMntXuFO3fx2IdJ0AWLJopYJiC5u42cT9ha74EQaDUajfX19WQyOTIyArhl3ikXQJGdOHj6yPxU0+HPLw6Fh0wrKyvnz59PpVKVSqXdbnd2KAxDuKEhoLeyr1sihq6iGIuOQhaiLJAQSSbzyZMnv//977/wwgsnT54slUrpdHp9fT0SupDzaDW0YSG2w6QeQyHNbAQ5gvx4PP7QQw8FQQCGBe9Dw0G4Q5pZyDrZkCTHKdtPZENIFFuLgqUpypa2m6K+973v5XI5eGgMsu+BgQG0t4eWKQwIECyA6VX7BxKO8GflHImNCDY5gvyRkZG7d++Ojo4C+APpp1mAx5w8PZS2awh3EsxdlikEJjnhEwqz2Sw8amF2PJKazebq6moymRweHjavAeEySH8o9MEY1oPKroDATZeWlsKdT2olEglwNYAr0yPptCHcsgU7OVAiZkQo0WFjsXwFR32AthuGof6MnjwjWkgQBDBu8Xi8Xq+br580m80wDOHL0PB87APIscwfdHDlvIQtsZGN8/nnn//b3/52/fp1MKz4DiUSib6+PjkBp3NmA1EXYJMBTJZgNsTajdrtG8J1rqOG1WoVMlH4urFptbVazfO8bDYL5ehzB47kPrMs7TKsyN72NhwyZzwef/HFF5999tk7d+5sbW1tb29XKpXt7e2tra1CoYAmRu0gWbetHxq5LECYONrzRSt8DkO/OGmKrdVqfX19urC3Udrj2PJf/7KRHC9kNhpTzAgyMDAwMDBgModhePHiRUUW99jWtRyaO6OmbQm1IgmyS1RF6ZQjqtlK2DgrXAGYb+Tq8nq9HgSBjowyXPUAS46mIu3dQMnG4O6mcjpiMnueh5ayZMnssVnLvacCMCPUNKWxI86ahcYqoV2EyvSqEO78pFIp9Cmezc1NpZT+OrVsWD3AkqOpJAQDpG7qmGO5+IELTzKZNLfvUbt9mv0sNCWKxxQCaRqEypEotgl9LJzqLedjEQvsKZlMwnMQsF9DGIawV0MZJiQAAA21SURBVA/Af88JlqyVy7B38YVVli0SQmzkpFwi0Ww2EfAoMSJT+WbqjaAiMgEyf5qtoGmG2waRywR7SQ2pa4E9wVWO3hDK87ylpaV4PK43Q9x3w3JU/v4r9i7J016ohwQunU7DplCKA5VuE5pINWwhVRHXQsLj8bhef6LybXAoK28LEZogr4JsAe5VQBOw99PExASURL73e0CEP3limz86KHIK3JUotXvC9HE6nbY1Cq8+w5U2Oiuo6nGPK2mSTR/1y+xRIpHQu1gp0abRgUCRzBp6zY0Rq9Xq6urq4OBgsVj0PK/T6ehFQXkGbTkA60isMgiPmReMbJGCZhi2nEPXtWEAW5H+zOVybJfCMASQoNuK2CK7HP5sFGkiQKlUCu4WuEtGE8Ymf9RFqRqQWmkju3nzZqfTmZ6ehirmNwqQQLNdeiyfEnqkaa9XhY7z1FtFWMdSBIrCMIS8XvhaLpuZodZZf6UVNZlwZfKkUim4xRmZ+aG+ywHXPBZ6YRb++9//jsfj8ES/t/uymtY6UOri0737mG85NtHf3w/vv1OUTiQS6F0DF3Sx2bdjuNf2rTdMA8pkMvAYmXs3HTkd5UAv7ty5c+/evb6+vlOnTukYvS8N9UDMp3uFbmtmG3RHxnJK6KwpHDZJa7VaKCjAi4HwVqqgqk1/gQ2FDKohnDJ34lNK5XI53/fhDp1Nn4Mgsy/tdvutt94Kw/D8+fPgirBjPpx1mRchaRbMA026/onf0kHqsihFERsddBv4hCytVCotLi7Si4BkMqlDT2S32f4rMX0xiabk5lVYGIb5fB52DKS5o22qbA3RurIn6MJ333337t276XT6mWeegVqwgmq2GDkvNsXkfAsJ91Dybl6GoAljswSXWbGZM/2J/up2x8bGFhYWTGalFNyx17NLu8B2iu0RAkuhI+ZPtKN/X19fq9XSKyORYyKc1RI8z6O5NmWDg6tXr8KrKBcuXNBXPOZHgelkuQQWx+CDrAUo4UXBj3lgdtLmBGyrtlrmTyrQ87zx8XG4AEQDpCeVjWWyMnukcPdTU57n9ff3+74P91JoE9T6kWvZapmzaItl9Xr98uXL169fb7VaIyMjL7zwApTDOz/KPrZso72VsOW71oFcInFkNuaeY9l8yDyIxWKnTp26efMmcjvbtT2S4KKGss+rTb7ezw0KBwcHfd9fX18X0IUKN1UVggNqWh+02+1r167961//2tzchJuG3/72t/WFjoYr2tPI0M8ikMDMTiLzaHJXRionYUIJ66ksWJ49e/b27dvmpvie58HVYrvdtl34RE6wY2pFNQyNzUshiYHXHzY2NtB+z7Ji9EBm1gqvrKxcu3Ztdna21Wrpp5C/853vDA4OAgNshOQILZFNd8vpmmMJERqxKeKLikM4hPDKbvVwkEwmz507d/XqVb2PKASjvr6+SqViPmzTMzl6pya0zh6Lxaampra2tpaWltD2cXtvSynleV6n07l9+/Z//vOf27dv12o1WJIFZxseHn755ZeHhoa0cHOTJiEVZgFFAHV3UaqrHIs1zMhC9pjmFjJ0nTx5cnV19fbt2/rhtU6nk8vllpaWBgcHTRPflyxKIB0KYald4+VDDz309ttv37p1Sy9O2i4skIYsLuqObG1tffjhh59++unS0lIsFoPN6OPxOCxtZLPZp5566sKFC+ZuR77vDw8PO6bC7tGpW1H4XqHJF3l1IyQ0LCciCmzILcyKTz/9dLvdvnv3rp5LM9GxpcA9k81GvZ3LUlhq13czz549e+nSpWvXrv3f//2fEv1eLoF2l5aW3n333bm5uc3Nzf7+/nQ6nc/n4asIkFzm8/lz5849/fTTfX19yui+7/tQoqWpqJmKPCt0BwkxJeDHZmxgYxPXLaejKBY1L1y4kMlkZmdnwUEhwTLTLKH/tthtVqR9p1iohw8ikeZ87LHHksnkrVu35ubmZmZmTGn00seWNvi+f/PmzatXr96+fbtYLKZSqeHh4SAI6vU6LOun0+kzZ8488sgjZ86c0dvRaN3a7XYymTT3vHDM5yLn0cVj0cThB/3Mn46GbFZHQykHZrV79GmLNAk4f/58Npv94IMP4JbOwMBAuVweGRlxka+4OQYeb+fSjPad1tUXZfqDUJ7nlUqls2fPzs/Pv/766z/+8Y+7egqqXC7Pzc3duXNnYWEBmi4UCrA9PSgDd2lOnz598uTJVCpFLyHDnRWQUqmE/Ecgc3jRmLMjoJTkn4hn19MBkbBh00xAeCRKDjGRHuZ53sMPP5zJZK5cuaKUisVi+oZPz8Ram7KPO7BBKNRbAXie95WvfGVpaenWrVtvvPHGiy++KHu/7/ufffbZ/Pz87du3t7e3PeNiE4Id7Ko6MzMzPT2tXxtEtqK1go9cjI6OdhVwEI9tymQ2G8/nW95QjWk3WJ0QILEVWURE5Up0C7McjldWVt5++21Ytkkmk+haTIbbfaFEItHf3z88PAwJdRiGrVbrl7/8ZbPZTKfTFy5c+NrXvqafzgCGMAxXV1c/++yzzz77bGFhQS/f610FPM8rFAonT548ffr0iRMncrkcGwfMgQqCoFqtZjIZuCpUnA/IMEPDgjx9ZgmLap/Xcn/vltLBTZsLbW5uvvnmm5lMplwuT0xMmKcOQSuIUIODg3BZCoX//Oc///CHP2QyGXh/ZmpqanBwMBaLNZvNSqVSLpf1apy+rlRKxWKx0dHR06dPT09Pj46OoqcXKenQ3Gw26/X6yMhI5BrH4dN9xKKWq0SLtgGPsiMWNXAaImn0UZxPQEmtVnvrrbfgHWLzaqg3ikzU1G6XhY9AjYyM6OfpwjB89dVX33//fTAs+HidPhUaK6ue52UyGcic4Ps/FNdt6sEuvdVqtVgsQuuKCx1sdkvjg0ACsJkMfENCA7Z8qFuevZAgH061Wq333ntvYWFhcHCQXYVnzSKyUZez8K728PCwCRhBEFy8ePHy5cvwCg283q2rwD4LMzMzMzMz4+Pjkbttmy222+1Wq1Wr1TqdDkRh9+fZD3qaKEmIZQMqJa6Y24I05ddnlQXkaAkrIQiCa9eu3bhxAz76xcqPLEQljg6dzWb7+/tLpRLAhhaysrJy5cqVe/fu+b6fTqfBDsbHxycmJvL5PMVm3SjqIFwlNBoN2JYtn88PDg6adowQTsAnW6Bgx4HqJowMG0/2lGMdKZqdnf3444+LxSLde5OSo7UJEoDg4xelUonNcswgSE+xQR8IvoQA9pRIJAqFQn9/f+Q3YI8U4atCWwrFV+ZWvATEYuvCTxb5FIFGAVY9z1tfX//ggw8ajQbay1q3gqzExZLkn3CbJZfLlUolOa7ZorA5YjrYNZvNbDbb19fX399v7p+mLJgkg5/7jFBRbNoXCeefI9ZBX9wd8sXj2trawsLC5uZmpVLpdDqxHfJ2kxLNzt3m0ul0Op0GUEGbc2hOoftwdwiScaVULpcrFovFYrH3/h8NwjkWzfMpSCguG6A5strtQLQu2yKLZGwCTg9Q60opSFDgI9DwvRP4fHyz2Ww2m3Dxz1qe6sbs4KtPhUKhWCyiOy3h7lV7TZ1OB55mhk+gFwoF+GgZHT0BjdgUCp2iDGYvhCtHJAQFJcUZya6/X5gcqweCVAbMDmxOE3w9Gp64QpanAOp3Gx+89wEffkqn0+j6FEYf1q5g8cn3fR3sHtTLygdKTCikmROUC3bNWjFiUxbwM0nAJ7OE9RhZlA3eKL+WD0GqYRAyO3jIDt5Z8DwvlUrBE38QHHU52BPAJHzMp1gswvOAMjDLqGwDeHOQKUqhkUe5NRsuTAZlIWZGDiHHOvrU2wiEO2vf9XpdG1wYhrBwBQew82UmkwF7+kKCE0v/6yZ1TAdE+7zHzTEdE9D970EAQalZaPuJOE2hpjQk3yS2Cltu42ThFmlrk0N7TYXQYxdRQhBAI2w7sA01Ox02tbtS1WVwbP1iex0RCsNDv8fkQkdTq2My6TjHOqYDoe5yrMO3wmO7/y+lWLg7bEMpjeI0eCtLCFckGzALBYE2aVS47QC1Lld0NFkqLVJPdqzkFtlWbLXc9adpmU1Vmkt12y46exwKj+lA6Hi54ZgOhPZqWMeAd0ws4XUsulJiFlIeVM5yKi7YR2YVNk7KZhNFNUQ8SCxSGB3QurS/NuFCE6aEyIFCPIJiVD7qC5UvjGHk3FGt7j/XcbwydEz7SLs+dG7zHsSASgRHp95AZVKBVILg6zaZrO/aHJc2bes4eypy3Cgz1Yf2Vx4uOnRsr9kR60ErVIX9aYr6r1x5P6ajTxHrWPRBRFRftmWTjfXLSP1YV6OiImuxWlHJtlqog2wXXEQhbBB6LSgvV9yX8sgqQvedEOuYjqk3woglAJjirNgGAzaHc/FXJKcHUawzRQb9rk45QohjE5RH0FnAexbaBWn7KAqxHSPWMR0IMTmWDRJY03YsjBRlcx1BuA1fTWksrpgHMv5ROTJMsiWoOVZbF8XU7tGTR8Y2VqxwYeRZUWyXkSjrM+89PwjPvk7Um6hDo33U8Oh39nAIf/KELi7Ql3DMs2wVZd8cQRDFCvzc/O3vJ/YsyqZwSF566arjNlHK8G9Wf8ps+yn019ZBW3WhFy6jSo91yV7dS9DsmP6XiV/HAkJhlY3ELAn5h5CjsE2wJZHNseXu+keK2osQR/2FFm05llCrq6ZdREVq9f9fHPLDsj8W4AAAAABJRU5ErkJggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1605630092, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW099 Smart Dimmer 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0103", "NodeProductID": "0x0063", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 2, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Light Dimmer Switch", "NodeDeviceType": 1536, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ]} +OpenZWave/1/node/12/instance/1/,{ "Instance": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/112/value/844425141682196/,{ "Label": "Current Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Deactivate Overload Protection (Default)" }, { "Value": 1, "Label": "Active Overload Protection" } ], "Selected": "Deactivate Overload Protection (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 12, "Genre": "Config", "Help": "Load will be closed when the Current overruns (US 15.5A, Others 16.2) for more than 2 minutes", "ValueIDKey": 844425141682196, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157} +OpenZWave/1/node/12/instance/1/commandclass/112/value/5629499745763348/,{ "Label": "Output Load Status", "Value": { "List": [ { "Value": 0, "Label": "Last status (Default)" }, { "Value": 1, "Label": "Always on" }, { "Value": 2, "Label": "Always off" } ], "Selected": "Last status (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 12, "Genre": "Config", "Help": "Configure the output load status after re-power on.", "ValueIDKey": 5629499745763348, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157} +OpenZWave/1/node/12/instance/1/commandclass/112/value/22517998348402708/,{ "Label": "Notification status", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Hail" }, { "Value": 2, "Label": "Basic" } ], "Selected": "Nothing", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 12, "Genre": "Config", "Help": "Defines the automated status notification of an associated device when status changes", "ValueIDKey": 22517998348402708, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} +OpenZWave/1/node/12/instance/1/commandclass/112/value/22799473325113364/,{ "Label": "Configure the state of the LED", "Value": { "List": [ { "Value": 0, "Label": "The LED will follow the status (on/off) of its load. (Default)" }, { "Value": 1, "Label": "When the state of the Switch changes, the LED will follow the status (on/off) of its load, but the LED will turn off after 5 seconds." }, { "Value": 2, "Label": "Night Light Mode" } ], "Selected": "The LED will follow the status (on/off) of its load. (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 12, "Genre": "Config", "Help": "Configure what the LED Ring displays during operations", "ValueIDKey": 22799473325113364, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} +OpenZWave/1/node/12/instance/1/commandclass/112/value/23362423278534675/,{ "Label": "Night Light Color", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 83, "Node": 12, "Genre": "Config", "Help": "Configure the RGB Value when in Night Light Mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23362423278534675, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} +OpenZWave/1/node/12/instance/1/commandclass/112/value/23643898255245331/,{ "Label": "RGB Brightness in Energy Mode", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 12, "Genre": "Config", "Help": "Configure the brightness level of RGB LED (0%-100%) when it is in Energy Mode/momentary indicate mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23643898255245331, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} +OpenZWave/1/node/12/instance/1/commandclass/112/value/25332748115509264/,{ "Label": "Enables/disables parameter 91/92", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 90, "Node": 12, "Genre": "Config", "Help": "Enable/disable Wattage threshold and percent.", "ValueIDKey": 25332748115509264, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} +OpenZWave/1/node/12/instance/1/commandclass/112/value/25614223092219926/,{ "Label": "Minimum Change to send Report (Watt)", "Value": 25, "Units": "watts", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 91, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum change in wattage for a Report to be sent (default 25 W)", "ValueIDKey": 25614223092219926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} +OpenZWave/1/node/12/instance/1/commandclass/112/value/25895698068930577/,{ "Label": "Minimum Change to send Report (%)", "Value": 5, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 92, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum percentage change in wattage for a Report to be sent (Default 5)", "ValueIDKey": 25895698068930577, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} +OpenZWave/1/node/12/instance/1/commandclass/112/value/28147497882615832/,{ "Label": "Default Group Reports", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 12, "Genre": "Config", "Help": "Set report types for groups 1, 2 and 3 to default.", "ValueIDKey": 28147497882615832, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/112/value/28428972859326483/,{ "Label": "Report type sent in Reporting Group 1", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28428972859326483, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} +OpenZWave/1/node/12/instance/1/commandclass/112/value/28710447836037139/,{ "Label": "Report type sent in Reporting Group 2", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 102, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28710447836037139, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} +OpenZWave/1/node/12/instance/1/commandclass/112/value/28991922812747795/,{ "Label": "Report type sent in Reporting Group 3", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 103, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28991922812747795, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} +OpenZWave/1/node/12/instance/1/commandclass/112/value/30962247649722392/,{ "Label": "Set 111 to 113 to default", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 110, "Node": 12, "Genre": "Config", "Help": "Set time interval for sending reports for groups 1, 2 and 3 to default.", "ValueIDKey": 30962247649722392, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/112/value/31243722626433043/,{ "Label": "Send Interval for Reporting Group 1", "Value": 3, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 1 is sent.", "ValueIDKey": 31243722626433043, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} +OpenZWave/1/node/12/instance/1/commandclass/112/value/31525197603143699/,{ "Label": "Send Interval for Reporting Group 2", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 112, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 2 is sent.", "ValueIDKey": 31525197603143699, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} +OpenZWave/1/node/12/instance/1/commandclass/112/value/31806672579854355/,{ "Label": "Send Interval for Reporting Group 3", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 113, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 3 is sent.", "ValueIDKey": 31806672579854355, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} +OpenZWave/1/node/12/instance/1/commandclass/112/value/56294995553681428/,{ "Label": "Partner ID", "Value": { "List": [ { "Value": 0, "Label": "Aeon Labs Standard (Default)" }, { "Value": 1, "Label": "Others" } ], "Selected": "Aeon Labs Standard (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 200, "Node": 12, "Genre": "Config", "Help": "Partner ID", "ValueIDKey": 56294995553681428, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} +OpenZWave/1/node/12/instance/1/commandclass/112/value/70931694342635540/,{ "Label": "Configuration Locked", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 12, "Genre": "Config", "Help": "Enable/disable Configuration Locked", "ValueIDKey": 70931694342635540, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} +OpenZWave/1/node/12/instance/1/commandclass/112/value/71494644296056854/,{ "Label": "Device tag", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 254, "Node": 12, "Genre": "Config", "Help": "Device tag.", "ValueIDKey": 71494644296056854, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629161} +OpenZWave/1/node/12/instance/1/commandclass/112/value/71776119272767512/,{ "Label": "Reset device", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 12, "Genre": "Config", "Help": "Reset to the default configuration.", "ValueIDKey": 71776119272767512, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/38/value/1407375098085395/,{ "Label": "Instance 1: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085395, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630293} +OpenZWave/1/node/12/instance/1/commandclass/38/value/206143505/,{ "Label": "Instance 1: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143505, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630090} +OpenZWave/1/node/12/instance/1/commandclass/38/value/281475182854168/,{ "Label": "Instance 1: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854168, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/38/value/562950159564824/,{ "Label": "Instance 1: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564824, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/38/value/844425144664080/,{ "Label": "Instance 1: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664080, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/38/value/1125900121374737/,{ "Label": "Instance 1: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374737, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/39/value/214548500/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 214548500, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} +OpenZWave/1/node/12/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/51/value/562950168166419/,{ "Label": "Color Channels", "Value": 28, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 12, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950168166419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/51/value/206356503/,{ "Label": "Color", "Value": "#000000", "Units": "#RRGGBB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 12, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 206356503, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} +OpenZWave/1/node/12/instance/1/commandclass/51/value/281475183067156/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Off", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 12, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475183067156, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} +OpenZWave/1/node/12/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/94/value/215449617/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449617, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/94/value/281475192160278/,{ "Label": "Instance 1: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160278, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/94/value/562950168870934/,{ "Label": "Instance 1: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870934, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/114/value/215777299/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 12, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 215777299, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/114/value/281475192487955/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 12, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475192487955, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/114/value/562950169198611/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 12, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950169198611, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/114/value/1125900122619927/,{ "Label": "Serial Number", "Value": "0a000100010106040700000108010000000000", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 12, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900122619927, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/215793684/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 215793684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} +OpenZWave/1/node/12/instance/1/commandclass/115/value/281475192504337/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 12, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475192504337, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} +OpenZWave/1/node/12/instance/1/commandclass/115/value/562950169215000/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 12, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950169215000, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/844425145925649/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425145925649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/1125900122636308/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900122636308, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/1407375099346966/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375099346966, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/1688850076057624/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 12, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850076057624, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/1970325052768280/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 12, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325052768280, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/2251800029478932/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 12, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800029478932, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/115/value/2533275006189590/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 12, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275006189590, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/129/value/207634452/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Tuesday", "Selected_id": 2 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 12, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 207634452, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} +OpenZWave/1/node/12/instance/1/commandclass/129/value/281475184345105/,{ "Label": "Hour", "Value": 11, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 12, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475184345105, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} +OpenZWave/1/node/12/instance/1/commandclass/129/value/562950161055761/,{ "Label": "Minute", "Value": 21, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 12, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950161055761, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630092} +OpenZWave/1/node/12/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/134/value/216104983/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 12, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 216104983, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/134/value/281475192815639/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 12, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475192815639, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/134/value/562950169526295/,{ "Label": "Application Version", "Value": "1.12", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 12, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950169526295, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/50/value/206340114/,{ "Label": "Electric - kWh", "Value": 17.562999725341798, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206340114, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} +OpenZWave/1/node/12/instance/1/commandclass/50/value/562950159761426/,{ "Label": "Electric - W", "Value": 9.6899995803833, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 562950159761426, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} +OpenZWave/1/node/12/instance/1/commandclass/50/value/1125900113182738/,{ "Label": "Electric - V", "Value": 123.04900360107422, "Units": "V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1125900113182738, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} +OpenZWave/1/node/12/instance/1/commandclass/50/value/1407375089893394/,{ "Label": "Electric - A", "Value": 0.08299999684095383, "Units": "A", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1407375089893394, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091} +OpenZWave/1/node/12/instance/1/commandclass/50/value/72057594244268048/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 72057594244268048, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091} +OpenZWave/1/node/12/instance/1/commandclass/50/value/72339069229367320/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 12, "Genre": "System", "Help": "", "ValueIDKey": 72339069229367320, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/43/value/206225427/,{ "Label": "Scene", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206225427, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/43/value/281475182936083/,{ "Label": "Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 281475182936083, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/1/commandclass/152/value/216399888/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399888, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/,{ "Instance": 2, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/38/value/1407375098085411/,{ "Label": "Instance 2: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085411, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630295} +OpenZWave/1/node/12/instance/2/commandclass/38/value/206143521/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": true, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143521, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630132} +OpenZWave/1/node/12/instance/2/commandclass/38/value/281475182854184/,{ "Label": "Instance 2: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854184, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/38/value/562950159564840/,{ "Label": "Instance 2: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564840, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/38/value/844425144664096/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664096, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/38/value/1125900121374753/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374753, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/94/value/215449633/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449633, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/94/value/281475192160294/,{ "Label": "Instance 2: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160294, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/94/value/562950168870950/,{ "Label": "Instance 2: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870950, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028} +OpenZWave/1/node/12/instance/2/commandclass/152/value/216399904/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399904, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} +OpenZWave/1/node/12/association/1/,{ "Name": "LifeLine", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1605629028} +OpenZWave/1/node/12/association/2/,{ "Name": "Retransmit Switch CC", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1605629035} \ No newline at end of file From a1306059af8b92e876fc7c32ea8f3b76fc8604b8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 19 Nov 2020 00:11:12 +0000 Subject: [PATCH 121/430] [ci skip] Translation update --- .../components/binary_sensor/translations/nl.json | 3 +++ homeassistant/components/bsblan/translations/sl.json | 11 +++++++++++ homeassistant/components/dunehd/translations/nl.json | 3 ++- homeassistant/components/dunehd/translations/sl.json | 7 +++++++ .../components/forked_daapd/translations/sl.json | 7 +++++++ .../components/gogogate2/translations/sl.json | 11 +++++++++++ .../components/guardian/translations/sl.json | 12 ++++++++++++ .../components/homekit/translations/nl.json | 3 ++- homeassistant/components/openuv/translations/sl.json | 3 +++ .../components/pi_hole/translations/sl.json | 5 ++++- homeassistant/components/plex/translations/sl.json | 2 +- .../components/plugwise/translations/sl.json | 7 +++++++ homeassistant/components/sonarr/translations/sl.json | 12 ++++++++++++ homeassistant/components/vizio/translations/es.json | 1 + homeassistant/components/vizio/translations/pl.json | 1 + 15 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/bsblan/translations/sl.json create mode 100644 homeassistant/components/dunehd/translations/sl.json create mode 100644 homeassistant/components/forked_daapd/translations/sl.json create mode 100644 homeassistant/components/gogogate2/translations/sl.json create mode 100644 homeassistant/components/guardian/translations/sl.json create mode 100644 homeassistant/components/plugwise/translations/sl.json create mode 100644 homeassistant/components/sonarr/translations/sl.json diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index e99c41a473c..dc8ff4ec511 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -122,6 +122,9 @@ "off": "Normaal", "on": "Heet" }, + "light": { + "on": "Licht gedetecteerd" + }, "lock": { "off": "Vergrendeld", "on": "Ontgrendeld" diff --git a/homeassistant/components/bsblan/translations/sl.json b/homeassistant/components/bsblan/translations/sl.json new file mode 100644 index 00000000000..2bf2dd68b44 --- /dev/null +++ b/homeassistant/components/bsblan/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Vrata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json index 91325588b13..c8e16770db2 100644 --- a/homeassistant/components/dunehd/translations/nl.json +++ b/homeassistant/components/dunehd/translations/nl.json @@ -5,7 +5,8 @@ }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "invalid_host": "ongeldige host of IP adres" }, "step": { "user": { diff --git a/homeassistant/components/dunehd/translations/sl.json b/homeassistant/components/dunehd/translations/sl.json new file mode 100644 index 00000000000..ccab4d05b4c --- /dev/null +++ b/homeassistant/components/dunehd/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_host": "Neveljavno ime gostitelja ali neveljaven IP naslov" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/sl.json b/homeassistant/components/forked_daapd/translations/sl.json new file mode 100644 index 00000000000..1c59e4bc9c7 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown_error": "Nepri\u010dakovana napaka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/sl.json b/homeassistant/components/gogogate2/translations/sl.json new file mode 100644 index 00000000000..78e46cfb9e3 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP naslov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sl.json b/homeassistant/components/guardian/translations/sl.json new file mode 100644 index 00000000000..f3542bb5899 --- /dev/null +++ b/homeassistant/components/guardian/translations/sl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP naslov", + "port": "Vrata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 487544cd5b5..ca41ff6758c 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -37,7 +37,8 @@ }, "init": { "data": { - "include_domains": "Op te nemen domeinen" + "include_domains": "Op te nemen domeinen", + "mode": "modus" }, "description": "HomeKit kan worden geconfigureerd om een brug of een enkel accessoire te tonen. In de accessoiremodus kan slechts \u00e9\u00e9n entiteit worden gebruikt. De accessoiremodus is vereist om mediaspelers met de tv-apparaatklasse correct te laten werken. Entiteiten in de \"Op te nemen domeinen\" zullen worden blootgesteld aan HomeKit. U kunt op het volgende scherm selecteren welke entiteiten u wilt opnemen of uitsluiten van deze lijst.", "title": "Selecteer domeinen om zichtbaar te maken." diff --git a/homeassistant/components/openuv/translations/sl.json b/homeassistant/components/openuv/translations/sl.json index cbd73cc9ddc..8a07b7a4a20 100644 --- a/homeassistant/components/openuv/translations/sl.json +++ b/homeassistant/components/openuv/translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Lokacija je \u017ee nastavljena" + }, "error": { "invalid_api_key": "Neveljaven API klju\u010d" }, diff --git a/homeassistant/components/pi_hole/translations/sl.json b/homeassistant/components/pi_hole/translations/sl.json index cd46d19f38c..f8f2b1e05e4 100644 --- a/homeassistant/components/pi_hole/translations/sl.json +++ b/homeassistant/components/pi_hole/translations/sl.json @@ -3,7 +3,10 @@ "step": { "user": { "data": { - "location": "Lokacija" + "location": "Lokacija", + "name": "Ime", + "ssl": "Uporablja SSL certifikat", + "verify_ssl": "Preverite SSL certifikat" } } } diff --git a/homeassistant/components/plex/translations/sl.json b/homeassistant/components/plex/translations/sl.json index 5a7ae2db621..b1622219402 100644 --- a/homeassistant/components/plex/translations/sl.json +++ b/homeassistant/components/plex/translations/sl.json @@ -21,7 +21,7 @@ "port": "Vrata", "ssl": "Uporaba SSL", "token": "\u017deton (izbirno)", - "verify_ssl": "Preverite SSL potrdilo" + "verify_ssl": "Preverite SSL certifikat" }, "title": "Ro\u010dna konfiguracija Plex" }, diff --git a/homeassistant/components/plugwise/translations/sl.json b/homeassistant/components/plugwise/translations/sl.json new file mode 100644 index 00000000000..8a0996ed92e --- /dev/null +++ b/homeassistant/components/plugwise/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Nepri\u010dakovana napaka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/sl.json b/homeassistant/components/sonarr/translations/sl.json new file mode 100644 index 00000000000..b8c5332be9c --- /dev/null +++ b/homeassistant/components/sonarr/translations/sl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ssl": "Uporablja SSL certifikat", + "verify_ssl": "Preverite SSL certifikat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index 244b9958360..d3ba768e933 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." }, "error": { diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 420a10c59bd..71754f6fbac 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa, aplikacje i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." }, "error": { From f1693e243383fb9f1ea90fdf6d1f946e16b57fc3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 18 Nov 2020 19:12:38 -0500 Subject: [PATCH 122/430] Refactor ZHA tests to allow attribute reads during device initialization (#43357) * Allow plugging zigpy attribute reads in tests * Migrate ZHA tests to use new patched attribute reads * Remove logging in tests --- tests/components/zha/common.py | 26 ++++++++++++++++++++++++-- tests/components/zha/test_climate.py | 14 +------------- tests/components/zha/test_cover.py | 20 +++++++------------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 11f390b202e..249e1bf58b2 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -70,12 +70,34 @@ FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group def patch_cluster(cluster): """Patch a cluster for testing.""" + cluster.PLUGGED_ATTR_READS = {} + + async def _read_attribute_raw(attributes, *args, **kwargs): + result = [] + for attr_id in attributes: + value = cluster.PLUGGED_ATTR_READS.get(attr_id) + if value is None: + # try converting attr_id to attr_name and lookup the plugs again + attr_name = cluster.attributes.get(attr_id) + value = attr_name and cluster.PLUGGED_ATTR_READS.get(attr_name[0]) + if value is not None: + result.append( + zcl_f.ReadAttributeRecord( + attr_id, + zcl_f.Status.SUCCESS, + zcl_f.TypeValue(python_type=None, value=value), + ) + ) + else: + result.append(zcl_f.ReadAttributeRecord(attr_id, zcl_f.Status.FAILURE)) + return (result,) + cluster.bind = AsyncMock(return_value=[0]) cluster.configure_reporting = AsyncMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() - cluster.read_attributes = AsyncMock(return_value=[{}, {}]) - cluster.read_attributes_raw = Mock() + cluster.read_attributes = AsyncMock(wraps=cluster.read_attributes) + cluster.read_attributes_raw = AsyncMock(side_effect=_read_attribute_raw) cluster.unbind = AsyncMock(return_value=[0]) cluster.write_attributes = AsyncMock( return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]] diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index fe69e6536d3..cc152c1a36d 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -140,19 +140,8 @@ def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): else: plugged_attrs = {**ZCL_ATTR_PLUG, **plug} - async def _read_attr(attrs, *args, **kwargs): - res = {} - failed = {} - - for attr in attrs: - if attr in plugged_attrs: - res[attr] = plugged_attrs[attr] - else: - failed[attr] = zcl_f.Status.UNSUPPORTED_ATTRIBUTE - return res, failed - zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf) - zigpy_device.endpoints[1].thermostat.read_attributes.side_effect = _read_attr + zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs zha_device = await zha_device_joined(zigpy_device) await async_enable_traffic(hass, [zha_device]) await hass.async_block_till_done() @@ -1039,7 +1028,6 @@ async def test_occupancy_reset(hass, device_climate_sinope): state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY - thrm_cluster.read_attributes.return_value = [True], {} await send_attributes_report( hass, thrm_cluster, {"occupied_heating_setpoint": 1950} ) diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 783637d26d7..97fa5c7579d 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -32,7 +32,7 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, MagicMock, patch +from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events, mock_coro, mock_restore_cache @@ -104,19 +104,13 @@ def zigpy_keen_vent(zigpy_device_mock): async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): """Test zha cover platform.""" - async def get_chan_attr(*args, **kwargs): - return 100 - - with patch( - "homeassistant.components.zha.core.channels.base.ZigbeeChannel.get_attribute_value", - new=MagicMock(side_effect=get_chan_attr), - ) as get_attr_mock: - # load up cover domain - zha_device = await zha_device_joined_restored(zigpy_cover_device) - assert get_attr_mock.call_count == 2 - assert get_attr_mock.call_args[0][0] == "current_position_lift_percentage" - + # load up cover domain cluster = zigpy_cover_device.endpoints.get(1).window_covering + cluster.PLUGGED_ATTR_READS = {"current_position_lift_percentage": 100} + zha_device = await zha_device_joined_restored(zigpy_cover_device) + assert cluster.read_attributes.call_count == 2 + assert "current_position_lift_percentage" in cluster.read_attributes.call_args[0][0] + entity_id = await find_entity_id(DOMAIN, zha_device, hass) assert entity_id is not None From 70a348984563a6a79e008ebdc3cd8deb019ad6ee Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 18 Nov 2020 20:31:22 -0500 Subject: [PATCH 123/430] Update Zha dependencies (#43373) --- 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 0dfb9ab5098..60757f88098 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zha-quirks==0.0.46", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.27.0", + "zigpy==0.27.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.2", "zigpy-znp==0.2.2" diff --git a/requirements_all.txt b/requirements_all.txt index 9af0e884fb7..a7dc4388c4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2359,7 +2359,7 @@ zigpy-zigate==0.7.2 zigpy-znp==0.2.2 # homeassistant.components.zha -zigpy==0.27.0 +zigpy==0.27.1 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f05ffa82c8e..d5193acad20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1140,4 +1140,4 @@ zigpy-zigate==0.7.2 zigpy-znp==0.2.2 # homeassistant.components.zha -zigpy==0.27.0 +zigpy==0.27.1 From 54c4e9335fea750bd8c954cf568ef3665bfcd426 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 18 Nov 2020 21:34:12 -0500 Subject: [PATCH 124/430] Refactor ZHA sensor initialization (#43339) * Refactor ZHA sensors to use cached values after restart * Get attr from cluster, not channel * Run cached state through formatter method * Use cached values for div/multiplier for SmartEnergy channel * Restore batter voltage from cache * Refactor sensor to use cached values only * Update tests * Add battery sensor test --- .../components/zha/core/channels/base.py | 2 +- .../components/zha/core/channels/general.py | 49 +------- .../zha/core/channels/smartenergy.py | 39 +++--- .../components/zha/core/discovery.py | 3 +- homeassistant/components/zha/sensor.py | 115 +++++++----------- tests/components/zha/test_sensor.py | 58 ++++++++- 6 files changed, 128 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 9f2fe4f21bd..c6019c10843 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -208,7 +208,7 @@ class ZigbeeChannel(LogMixin): attributes = [] for report_config in self._report_config: attributes.append(report_config["attr"]) - if len(attributes) > 0: + if attributes: await self.get_attributes(attributes, from_cache=from_cache) self._status = ChannelStatus.INITIALIZED diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index f443151de02..8747355a21a 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -17,10 +17,9 @@ from ..const import ( SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, - SIGNAL_STATE_ATTR, SIGNAL_UPDATE_DEVICE, ) -from .base import ClientChannel, ZigbeeChannel, parse_and_log_command +from .base import ChannelStatus, ClientChannel, ZigbeeChannel, parse_and_log_command @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) @@ -72,13 +71,6 @@ class BasicChannel(ZigbeeChannel): 6: "Emergency mains and transfer switch", } - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType - ) -> None: - """Initialize BasicChannel.""" - super().__init__(cluster, ch_pool) - self._power_source = None - async def async_configure(self): """Configure this channel.""" await super().async_configure() @@ -87,16 +79,12 @@ class BasicChannel(ZigbeeChannel): async def async_initialize(self, from_cache): """Initialize channel.""" if not self._ch_pool.skip_configuration or from_cache: - power_source = await self.get_attribute_value( - "power_source", from_cache=from_cache - ) - if power_source is not None: - self._power_source = power_source + await self.get_attribute_value("power_source", from_cache=from_cache) await super().async_initialize(from_cache) def get_power_source(self): """Get the power source.""" - return self._power_source + return self.cluster.get("power_source") @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) @@ -392,38 +380,8 @@ class PowerConfigurationChannel(ZigbeeChannel): {"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE}, ) - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - attr = self._report_config[1].get("attr") - if isinstance(attr, str): - attr_id = self.cluster.attridx.get(attr) - else: - attr_id = attr - if attrid == attr_id: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - attrid, - self.cluster.attributes.get(attrid, [attrid])[0], - value, - ) - return - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_STATE_ATTR}", attr_name, value - ) - async def async_initialize(self, from_cache): """Initialize channel.""" - await self.async_read_state(from_cache) - await super().async_initialize(from_cache) - - async def async_update(self): - """Retrieve latest state.""" - await self.async_read_state(True) - - async def async_read_state(self, from_cache): - """Read data from the cluster.""" attributes = [ "battery_size", "battery_percentage_remaining", @@ -431,6 +389,7 @@ class PowerConfigurationChannel(ZigbeeChannel): "battery_quantity", ] await self.get_attributes(attributes, from_cache=from_cache) + self._status = ChannelStatus.INITIALIZED @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 120d0afdfb6..792b9413294 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -1,4 +1,6 @@ """Smart energy channels module for Zigbee Home Automation.""" +from typing import Union + import zigpy.zcl.clusters.smartenergy as smartenergy from homeassistant.const import ( @@ -82,44 +84,48 @@ class Metering(ZigbeeChannel): ) -> None: """Initialize Metering.""" super().__init__(cluster, ch_pool) - self._divisor = 1 - self._multiplier = 1 - self._unit_enum = None self._format_spec = None - async def async_configure(self): + @property + def divisor(self) -> int: + """Return divisor for the value.""" + return self.cluster.get("divisor") + + @property + def multiplier(self) -> int: + """Return multiplier for the value.""" + return self.cluster.get("multiplier") + + async def async_configure(self) -> None: """Configure channel.""" await self.fetch_config(False) await super().async_configure() - async def async_initialize(self, from_cache): + async def async_initialize(self, from_cache: bool) -> None: """Initialize channel.""" await self.fetch_config(True) await super().async_initialize(from_cache) @callback - def attribute_updated(self, attrid, value): + def attribute_updated(self, attrid: int, value: int) -> None: """Handle attribute update from Metering cluster.""" - if None in (self._multiplier, self._divisor, self._format_spec): + if None in (self.multiplier, self.divisor, self._format_spec): return - super().attribute_updated(attrid, value * self._multiplier / self._divisor) + super().attribute_updated(attrid, value) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return unit of measurement.""" - return self.unit_of_measure_map.get(self._unit_enum & 0x7F, "unknown") + uom = self.cluster.get("unit_of_measure", 0x7F) + return self.unit_of_measure_map.get(uom & 0x7F, "unknown") - async def fetch_config(self, from_cache): + async def fetch_config(self, from_cache: bool) -> None: """Fetch config from device and updates format specifier.""" results = await self.get_attributes( ["divisor", "multiplier", "unit_of_measure", "demand_formatting"], from_cache=from_cache, ) - self._divisor = results.get("divisor", self._divisor) - self._multiplier = results.get("multiplier", self._multiplier) - self._unit_enum = results.get("unit_of_measure", 0x7F) # default to unknown - fmting = results.get( "demand_formatting", 0xF9 ) # 1 digit to the right, 15 digits to the left @@ -135,8 +141,9 @@ class Metering(ZigbeeChannel): else: self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}" - def formatter_function(self, value): + def formatter_function(self, value: int) -> Union[int, float]: """Return formatted value for display.""" + value = value * self.multiplier / self.divisor if self.unit_of_measurement == POWER_WATT: # Zigbee spec power unit is kW, but we show the value in W value_watt = value * 1000 diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 25f320b0bf1..4dff2c6b16b 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -39,12 +39,13 @@ async def async_add_entities( Tuple[str, zha_typing.ZhaDeviceType, List[zha_typing.ChannelType]], ] ], + update_before_add: bool = True, ) -> None: """Add entities helper.""" if not entities: return to_add = [ent_cls(*args) for ent_cls, args in entities] - _async_add_entities(to_add, update_before_add=True) + _async_add_entities(to_add, update_before_add=update_before_add) entities.clear() diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 302637cc068..eff3892630b 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,6 +1,7 @@ """Sensors on Zigbee Home Automation networks.""" import functools import numbers +from typing import Any, Callable, Dict, List, Optional, Union from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, @@ -11,18 +12,17 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, DOMAIN, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, - STATE_UNKNOWN, TEMP_CELSIUS, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util.temperature import fahrenheit_to_celsius +from homeassistant.helpers.typing import HomeAssistantType, StateType from .core import discovery from .core.const import ( @@ -38,9 +38,9 @@ from .core.const import ( DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, - SIGNAL_STATE_ATTR, ) from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES +from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity PARALLEL_UPDATES = 5 @@ -65,7 +65,9 @@ CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the Zigbee Home Automation sensor from config entry.""" entities_to_create = hass.data[DATA_ZHA][DOMAIN] @@ -73,7 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, ), ) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) @@ -82,29 +87,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class Sensor(ZhaEntity): """Base ZHA sensor.""" - SENSOR_ATTR = None - _decimals = 1 - _device_class = None - _divisor = 1 - _multiplier = 1 - _unit = None + SENSOR_ATTR: Optional[Union[int, str]] = None + _decimals: int = 1 + _device_class: Optional[str] = None + _divisor: int = 1 + _multiplier: int = 1 + _unit: Optional[str] = None - def __init__(self, unique_id, zha_device, channels, **kwargs): + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: List[ChannelType], + **kwargs, + ): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._channel = channels[0] + self._channel: ChannelType = channels[0] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() - self._device_state_attributes.update(await self.async_state_attr_provider()) - self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) - self.async_accept_signal( - self._channel, SIGNAL_STATE_ATTR, self.async_update_state_attribute - ) @property def device_class(self) -> str: @@ -112,37 +118,25 @@ class Sensor(ZhaEntity): return self._device_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of this entity.""" return self._unit @property - def state(self) -> str: + def state(self) -> StateType: """Return the state of the entity.""" - if self._state is None: + assert self.SENSOR_ATTR is not None + raw_state = self._channel.cluster.get(self.SENSOR_ATTR) + if raw_state is None: return None - return self._state + return self.formatter(raw_state) @callback - def async_set_state(self, attr_id, attr_name, value): + def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: """Handle state update from channel.""" - if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name: - return - if value is not None: - value = self.formatter(value) - self._state = value self.async_write_ha_state() - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._state = last_state.state - - async def async_state_attr_provider(self): - """Initialize device state attributes.""" - return {} - - def formatter(self, value): + def formatter(self, value: int) -> Union[int, float]: """Numeric pass-through formatter.""" if self._decimals > 0: return round( @@ -167,7 +161,7 @@ class Battery(Sensor): _unit = PERCENTAGE @staticmethod - def formatter(value): + def formatter(value: int) -> int: """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1: @@ -175,26 +169,21 @@ class Battery(Sensor): value = round(value / 2) return value - async def async_state_attr_provider(self): + @property + def device_state_attributes(self) -> Dict[str, Any]: """Return device state attrs for battery sensors.""" state_attrs = {} - attributes = ["battery_size", "battery_quantity"] - results = await self._channel.get_attributes(attributes) - battery_size = results.get("battery_size") + battery_size = self._channel.cluster.get("battery_size") if battery_size is not None: state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") - battery_quantity = results.get("battery_quantity") + battery_quantity = self._channel.cluster.get("battery_quantity") if battery_quantity is not None: state_attrs["battery_quantity"] = battery_quantity + battery_voltage = self._channel.cluster.get("battery_voltage") + if battery_voltage is not None: + state_attrs["battery_voltage"] = round(battery_voltage / 10, 1) return state_attrs - @callback - def async_update_state_attribute(self, key, value): - """Update a single device state attribute.""" - if key == "battery_voltage": - self._device_state_attributes[key] = round(value / 10, 1) - self.async_write_ha_state() - @STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurement(Sensor): @@ -202,7 +191,6 @@ class ElectricalMeasurement(Sensor): SENSOR_ATTR = "active_power" _device_class = DEVICE_CLASS_POWER - _divisor = 10 _unit = POWER_WATT @property @@ -210,7 +198,7 @@ class ElectricalMeasurement(Sensor): """Return True if HA needs to poll for state changes.""" return True - def formatter(self, value) -> int: + def formatter(self, value: int) -> Union[int, float]: """Return 'normalized' value.""" value = value * self._channel.multiplier / self._channel.divisor if value < 100 and self._channel.divisor > 1: @@ -244,7 +232,7 @@ class Illuminance(Sensor): _unit = LIGHT_LUX @staticmethod - def formatter(value): + def formatter(value: int) -> float: """Convert illumination data.""" return round(pow(10, ((value - 1) / 10000)), 1) @@ -256,12 +244,12 @@ class SmartEnergyMetering(Sensor): SENSOR_ATTR = "instantaneous_demand" _device_class = DEVICE_CLASS_POWER - def formatter(self, value): + def formatter(self, value: int) -> Union[int, float]: """Pass through channel formatter.""" return self._channel.formatter_function(value) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return Unit of measurement.""" return self._channel.unit_of_measurement @@ -284,14 +272,3 @@ class Temperature(Sensor): _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 _unit = TEMP_CELSIUS - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - if last_state.state == STATE_UNKNOWN: - return - if last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) != TEMP_CELSIUS: - ftemp = float(last_state.state) - self._state = round(fahrenheit_to_celsius(ftemp), 1) - return - self._state = last_state.state diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 8d854894ba0..b6b4b343e3b 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -93,18 +93,59 @@ async def async_test_electrical_measurement(hass, cluster, entity_id): assert_state(hass, entity_id, "9.9", POWER_WATT) +async def async_test_powerconfiguration(hass, cluster, entity_id): + """Test powerconfiguration/battery sensor.""" + await send_attributes_report(hass, cluster, {33: 98}) + assert_state(hass, entity_id, "49", "%") + assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.9 + assert hass.states.get(entity_id).attributes["battery_quantity"] == 3 + assert hass.states.get(entity_id).attributes["battery_size"] == "AAA" + await send_attributes_report(hass, cluster, {32: 20}) + assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 + + @pytest.mark.parametrize( - "cluster_id, test_func, report_count", + "cluster_id, test_func, report_count, read_plug", ( - (measurement.RelativeHumidity.cluster_id, async_test_humidity, 1), - (measurement.TemperatureMeasurement.cluster_id, async_test_temperature, 1), - (measurement.PressureMeasurement.cluster_id, async_test_pressure, 1), - (measurement.IlluminanceMeasurement.cluster_id, async_test_illuminance, 1), - (smartenergy.Metering.cluster_id, async_test_metering, 1), + (measurement.RelativeHumidity.cluster_id, async_test_humidity, 1, None), + ( + measurement.TemperatureMeasurement.cluster_id, + async_test_temperature, + 1, + None, + ), + (measurement.PressureMeasurement.cluster_id, async_test_pressure, 1, None), + ( + measurement.IlluminanceMeasurement.cluster_id, + async_test_illuminance, + 1, + None, + ), + ( + smartenergy.Metering.cluster_id, + async_test_metering, + 1, + { + "demand_formatting": 0xF9, + "divisor": 1, + "multiplier": 1, + }, + ), ( homeautomation.ElectricalMeasurement.cluster_id, async_test_electrical_measurement, 1, + None, + ), + ( + general.PowerConfiguration.cluster_id, + async_test_powerconfiguration, + 2, + { + "battery_size": 4, # AAA + "battery_voltage": 29, + "battery_quantity": 3, + }, ), ), ) @@ -115,6 +156,7 @@ async def test_sensor( cluster_id, test_func, report_count, + read_plug, ): """Test zha sensor platform.""" @@ -128,6 +170,10 @@ async def test_sensor( } ) cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] + if cluster_id == smartenergy.Metering.cluster_id: + # this one is mains powered + zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 + cluster.PLUGGED_ATTR_READS = read_plug zha_device = await zha_device_joined_restored(zigpy_device) entity_id = await find_entity_id(DOMAIN, zha_device, hass) From f555e20fac5c9d534082dd6b1a829d61082370cd Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 Nov 2020 06:46:30 +0100 Subject: [PATCH 125/430] Update denonavr to 0.9.6 (#43370) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 86bee686764..7a00779dcca 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.5", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.6", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a7dc4388c4a..133f4f66e90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -484,7 +484,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.5 +denonavr==0.9.6 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5193acad20..d64630befbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ debugpy==1.1.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.5 +denonavr==0.9.6 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 From 4205367aa7cd46ff51947392e62e0e94076d9ab8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 19 Nov 2020 02:46:20 -0500 Subject: [PATCH 126/430] Bump pyvizio to 0.1.57 (#43374) --- homeassistant/components/vizio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 7aa544b4a0b..9e4bd712e0f 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.56"], + "requirements": ["pyvizio==0.1.57"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 133f4f66e90..d2d9acef763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1876,7 +1876,7 @@ pyversasense==0.0.6 pyvesync==1.2.0 # homeassistant.components.vizio -pyvizio==0.1.56 +pyvizio==0.1.57 # homeassistant.components.velux pyvlx==0.2.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d64630befbd..24b01699aa0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -911,7 +911,7 @@ pyvera==0.3.11 pyvesync==1.2.0 # homeassistant.components.vizio -pyvizio==0.1.56 +pyvizio==0.1.57 # homeassistant.components.volumio pyvolumio==0.1.3 From 0bf9734af1a82556b51766ec133c259eb0071c2a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Nov 2020 09:07:35 +0100 Subject: [PATCH 127/430] Bump hatasmota to 0.0.32 (#43360) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_discovery.py | 19 +++++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index c270be6e633..6140de6025a 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.0.31"], + "requirements": ["hatasmota==0.0.32"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index d2d9acef763..d4a35de3e97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.0.31 +hatasmota==0.0.32 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24b01699aa0..de236d4f0fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -379,7 +379,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.0.31 +hatasmota==0.0.32 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index b6a51a38daf..40fecb6b695 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -22,6 +22,25 @@ async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota): assert call_args[2] == 0 +async def test_future_discovery_message(hass, mqtt_mock, caplog): + """Test we handle backwards compatible discovery messages.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["future_option"] = "BEST_SINCE_SLICED_BREAD" + config["so"]["another_future_option"] = "EVEN_BETTER" + + with patch( + "homeassistant.components.tasmota.discovery.tasmota_get_device_config", + return_value={}, + ) as mock_tasmota_get_device_config: + await setup_tasmota_helper(hass) + + async_fire_mqtt_message( + hass, f"{DEFAULT_PREFIX}/00000049A3BC/config", json.dumps(config) + ) + await hass.async_block_till_done() + assert mock_tasmota_get_device_config.called + + async def test_valid_discovery_message(hass, mqtt_mock, caplog): """Test discovery callback called.""" config = copy.deepcopy(DEFAULT_CONFIG) From 3dbfd2cb70f6a8da9506c36d30678b352a34335f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 19 Nov 2020 10:47:23 +0100 Subject: [PATCH 128/430] Add shelly installed firmware info (#43221) --- .../components/shelly/binary_sensor.py | 5 ++++- homeassistant/components/shelly/entity.py | 19 +++++++++++-------- homeassistant/components/shelly/utils.py | 13 +++++-------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 4f771a9cc46..00f63ea7411 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -64,7 +64,10 @@ REST_SENSORS = { icon="mdi:update", default_enabled=False, path="update/has_update", - attributes={"description": "available version:", "path": "update/new_version"}, + attributes=[ + {"description": "latest_stable_version", "path": "update/new_version"}, + {"description": "installed_version", "path": "update/old_version"}, + ], ), } diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index d0e51e8cc12..047c3d9d66a 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -297,17 +297,20 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return f"{self.wrapper.mac}-{self.description.path}" @property - def device_state_attributes(self): + def device_state_attributes(self) -> dict: """Return the state attributes.""" if self._attributes is None: return None - _description = self._attributes.get("description") - _attribute_value = get_rest_value_from_path( - self.wrapper.device.status, - self.description.device_class, - self._attributes.get("path"), - ) + attributes = dict() + for attrib in self._attributes: + description = attrib.get("description") + attribute_value = get_rest_value_from_path( + self.wrapper.device.status, + self.description.device_class, + attrib.get("path"), + ) + attributes[description] = attribute_value - return {_description: _attribute_value} + return attributes diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 10eacff7068..c9ef3f55adb 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -85,14 +85,11 @@ def get_rest_value_from_path(status, device_class, path: str): """Parser for REST path from device status.""" if "/" not in path: - _attribute_value = status[path] + attribute_value = status[path] else: - _attribute_value = status[path.split("/")[0]][path.split("/")[1]] + attribute_value = status[path.split("/")[0]][path.split("/")[1]] if device_class == DEVICE_CLASS_TIMESTAMP: - last_boot = datetime.utcnow() - timedelta(seconds=_attribute_value) - _attribute_value = last_boot.replace(microsecond=0).isoformat() + last_boot = datetime.utcnow() - timedelta(seconds=attribute_value) + attribute_value = last_boot.replace(microsecond=0).isoformat() - if "new_version" in path: - _attribute_value = _attribute_value.split("/")[1].split("@")[0] - - return _attribute_value + return attribute_value From 982624b3ac59e9356f38e98eaa2657a05b5a364b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 19 Nov 2020 12:42:24 +0200 Subject: [PATCH 129/430] Support for Shelly Binary Input Sensors (#43313) Co-authored-by: Maciej Bieniek Co-authored-by: Paulus Schoutsen --- .../components/shelly/binary_sensor.py | 20 +++++++++ homeassistant/components/shelly/entity.py | 16 ++++++- homeassistant/components/shelly/light.py | 6 +-- homeassistant/components/shelly/sensor.py | 44 ++++--------------- homeassistant/components/shelly/switch.py | 5 +-- homeassistant/components/shelly/utils.py | 37 +++++++++++----- 6 files changed, 73 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 00f63ea7411..62cd9aea8ce 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, + DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, @@ -18,6 +19,7 @@ from .entity import ( async_setup_entry_attribute_entities, async_setup_entry_rest, ) +from .utils import is_momentary_input SENSORS = { ("device", "overtemp"): BlockAttributeDescription( @@ -50,6 +52,24 @@ SENSORS = { ("sensor", "vibration"): BlockAttributeDescription( name="Vibration", device_class=DEVICE_CLASS_VIBRATION ), + ("input", "input"): BlockAttributeDescription( + name="Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + removal_condition=is_momentary_input, + ), + ("relay", "input"): BlockAttributeDescription( + name="Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + removal_condition=is_momentary_input, + ), + ("device", "input"): BlockAttributeDescription( + name="Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + removal_condition=is_momentary_input, + ), } REST_SENSORS = { diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 047c3d9d66a..9b834db923c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers import device_registry, entity, update_coordinator from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST -from .utils import get_entity_name, get_rest_value_from_path +from .utils import async_remove_shelly_entity, get_entity_name, get_rest_value_from_path async def async_setup_entry_attribute_entities( @@ -31,7 +31,17 @@ async def async_setup_entry_attribute_entities( if getattr(block, sensor_id, None) in (-1, None): continue - blocks.append((block, sensor_id, description)) + # Filter and remove entities that according to settings should not create an entity + if description.removal_condition and description.removal_condition( + wrapper.device.settings, block + ): + domain = sensor_class.__module__.split(".")[-1] + unique_id = sensor_class( + wrapper, block, sensor_id, description + ).unique_id + await async_remove_shelly_entity(hass, domain, unique_id) + else: + blocks.append((block, sensor_id, description)) if not blocks: return @@ -77,6 +87,8 @@ class BlockAttributeDescription: device_class: Optional[str] = None default_enabled: bool = True available: Optional[Callable[[aioshelly.Block], bool]] = None + # Callable (settings, block), return true if entity should be removed + removal_condition: Optional[Callable[[dict, aioshelly.Block], bool]] = None device_state_attributes: Optional[ Callable[[aioshelly.Block], Optional[dict]] ] = None diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 83c7f3cf177..b3a6869d67d 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -19,7 +19,7 @@ from homeassistant.util.color import ( from . import ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity -from .utils import async_remove_entity_by_domain +from .utils import async_remove_shelly_entity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -39,9 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = ( f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' ) - await async_remove_entity_by_domain( - hass, "switch", unique_id, config_entry.entry_id - ) + await async_remove_shelly_entity(hass, "switch", unique_id) if not blocks: return diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 81dc2ef1c11..10d15fdd62f 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,6 +1,4 @@ """Sensor for Shelly.""" -import logging - from homeassistant.components import sensor from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -14,8 +12,7 @@ from homeassistant.const import ( VOLT, ) -from . import ShellyDeviceWrapper, get_device_name -from .const import DATA_CONFIG_ENTRY, DOMAIN, REST, SHAIR_MAX_WORK_HOURS +from .const import SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -24,17 +21,15 @@ from .entity import ( async_setup_entry_attribute_entities, async_setup_entry_rest, ) -from .utils import async_remove_entity_by_domain, temperature_unit - -_LOGGER = logging.getLogger(__name__) - -BATTERY_SENSOR = { - ("device", "battery"): BlockAttributeDescription( - name="Battery", unit=PERCENTAGE, device_class=sensor.DEVICE_CLASS_BATTERY - ), -} +from .utils import temperature_unit SENSORS = { + ("device", "battery"): BlockAttributeDescription( + name="Battery", + unit=PERCENTAGE, + device_class=sensor.DEVICE_CLASS_BATTERY, + removal_condition=lambda settings, _: settings.get("external_power") == 1, + ), ("device", "deviceTemp"): BlockAttributeDescription( name="Device Temperature", unit=temperature_unit, @@ -176,6 +171,7 @@ REST_SENSORS = { "uptime": RestAttributeDescription( name="Uptime", device_class=sensor.DEVICE_CLASS_TIMESTAMP, + default_enabled=False, path="uptime", ), } @@ -183,28 +179,6 @@ REST_SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" - - wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][REST] - - if ( - "external_power" in wrapper.device.settings - and wrapper.device.settings["external_power"] == 1 - ): - _LOGGER.debug( - "Removed battery sensor [externally powered] for %s", - get_device_name(wrapper.device), - ) - unique_id = f'{wrapper.device.shelly["mac"]}-battery' - await async_remove_entity_by_domain( - hass, "sensor", unique_id, config_entry.entry_id - ) - else: - await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, BATTERY_SENSOR, ShellySensor - ) - await async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, SENSORS, ShellySensor ) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 653f090bf4e..c86487072c6 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from . import ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity -from .utils import async_remove_entity_by_domain +from .utils import async_remove_shelly_entity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -32,11 +32,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = ( f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' ) - await async_remove_entity_by_domain( + await async_remove_shelly_entity( hass, "light", unique_id, - config_entry.entry_id, ) if not relay_blocks: diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index c9ef3f55adb..e6981db2a0d 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -8,24 +8,20 @@ import aioshelly from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers import entity_registry from . import ShellyDeviceWrapper +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_remove_entity_by_domain(hass, domain, unique_id, config_entry_id): - """Remove entity by domain.""" - +async def async_remove_shelly_entity(hass, domain, unique_id): + """Remove a Shelly entity.""" entity_reg = await hass.helpers.entity_registry.async_get_registry() - for entry in entity_registry.async_entries_for_config_entry( - entity_reg, config_entry_id - ): - if entry.domain == domain and entry.unique_id == unique_id: - entity_reg.async_remove(entry.entity_id) - _LOGGER.debug("Removed %s domain for %s", domain, entry.original_name) - break + entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) + if entity_id: + _LOGGER.debug("Removing entity: %s", entity_id) + entity_reg.async_remove(entity_id) def temperature_unit(block_info: dict) -> str: @@ -92,4 +88,23 @@ def get_rest_value_from_path(status, device_class, path: str): last_boot = datetime.utcnow() - timedelta(seconds=attribute_value) attribute_value = last_boot.replace(microsecond=0).isoformat() + if "new_version" in path: + attribute_value = attribute_value.split("/")[1].split("@")[0] + return attribute_value + + +def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: + """Return true if input button settings is set to a momentary type.""" + button = settings.get("relays") or settings.get("lights") or settings.get("inputs") + + # Shelly 1L has two button settings in the first channel + if settings["device"]["type"] == "SHSW-L": + channel = int(block.channel or 0) + 1 + button_type = button[0].get("btn" + str(channel) + "_type") + else: + # Some devices has only one channel in settings + channel = min(int(block.channel or 0), len(button) - 1) + button_type = button[channel].get("btn_type") + + return button_type in ["momentary", "momentary_on_release"] From 3ca8f0c47552a3e983f07d04b8963f897b9c2a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 19 Nov 2020 11:55:21 +0100 Subject: [PATCH 130/430] Add back system_health_info to the base of lovelace (#43382) --- homeassistant/components/lovelace/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 000c86567ca..7d0fe6574b9 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -37,6 +37,7 @@ from .const import ( STORAGE_DASHBOARD_UPDATE_FIELDS, url_slug, ) +from .system_health import system_health_info # NOQA _LOGGER = logging.getLogger(__name__) From 390b45b14939340bf48e0db37a57736c4232d8fe Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 19 Nov 2020 12:00:01 +0100 Subject: [PATCH 131/430] Reword numeric_state trigger (#43367) --- .../homeassistant/triggers/numeric_state.py | 62 +++++++++---------- .../triggers/test_numeric_state.py | 25 ++++++++ 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index a6e3b33ae97..25b9a4417dc 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -63,7 +63,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) + entity_ids = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) time_delta = config.get(CONF_FOR) @@ -78,29 +78,32 @@ async def async_attach_trigger( if value_template is not None: value_template.hass = hass - @callback - def check_numeric_state(entity, from_s, to_s): - """Return True if criteria are now met.""" - if to_s is None: - return False - - variables = { + def variables(entity_id): + """Return a dict with trigger variables.""" + return { "trigger": { "platform": "numeric_state", - "entity_id": entity, + "entity_id": entity_id, "below": below, "above": above, "attribute": attribute, } } + + @callback + def check_numeric_state(entity_id, from_s, to_s): + """Return True if criteria are now met.""" + if to_s is None: + return False + return condition.async_numeric_state( - hass, to_s, below, above, value_template, variables, attribute + hass, to_s, below, above, value_template, variables(entity_id), attribute ) @callback def state_automation_listener(event): """Listen for state changes and calls action.""" - entity = event.data.get("entity_id") + entity_id = event.data.get("entity_id") from_s = event.data.get("old_state") to_s = event.data.get("new_state") @@ -112,38 +115,29 @@ async def async_attach_trigger( { "trigger": { "platform": platform_type, - "entity_id": entity, + "entity_id": entity_id, "below": below, "above": above, "from_state": from_s, "to_state": to_s, - "for": time_delta if not time_delta else period[entity], - "description": f"numeric state of {entity}", + "for": time_delta if not time_delta else period[entity_id], + "description": f"numeric state of {entity_id}", } }, to_s.context, ) - matching = check_numeric_state(entity, from_s, to_s) + matching = check_numeric_state(entity_id, from_s, to_s) if not matching: - entities_triggered.discard(entity) - elif entity not in entities_triggered: - entities_triggered.add(entity) + entities_triggered.discard(entity_id) + elif entity_id not in entities_triggered: + entities_triggered.add(entity_id) if time_delta: - variables = { - "trigger": { - "platform": "numeric_state", - "entity_id": entity, - "below": below, - "above": above, - } - } - try: - period[entity] = cv.positive_time_period( - template.render_complex(time_delta, variables) + period[entity_id] = cv.positive_time_period( + template.render_complex(time_delta, variables(entity_id)) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( @@ -151,20 +145,20 @@ async def async_attach_trigger( automation_info["name"], ex, ) - entities_triggered.discard(entity) + entities_triggered.discard(entity_id) return - unsub_track_same[entity] = async_track_same_state( + unsub_track_same[entity_id] = async_track_same_state( hass, - period[entity], + period[entity_id], call_action, - entity_ids=entity, + entity_ids=entity_id, async_check_same_func=check_numeric_state, ) else: call_action() - unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) + unsub = async_track_state_change_event(hass, entity_ids, state_automation_listener) @callback def async_remove(): diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 09a13f95603..c8a9cd6d50f 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -34,6 +34,31 @@ def setup_comp(hass): mock_component(hass, "group") +async def test_if_not_fires_on_entity_removal(hass, calls): + """Test the firing with removed entity.""" + hass.states.async_set("test.entity", 11) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "below": 10, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # Entity disappears + hass.states.async_remove("test.entity") + await hass.async_block_till_done() + assert len(calls) == 0 + + async def test_if_fires_on_entity_change_below(hass, calls): """Test the firing with changed entity.""" context = Context() From d61998e184f4bf40e48852bdf778b4f253c9779f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Nov 2020 12:05:52 +0100 Subject: [PATCH 132/430] Raise in base implementation of FanEntity.oscillate (#43354) --- homeassistant/components/fan/__init__.py | 1 + tests/components/fan/test_init.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 789299bd1ac..90a3d030703 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -144,6 +144,7 @@ class FanEntity(ToggleEntity): def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" + raise NotImplementedError() async def async_oscillate(self, oscillating: bool): """Oscillate the fan.""" diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index c481e9f0937..a8beed73a07 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -20,7 +20,8 @@ def test_fanentity(): assert fan.supported_features == 0 assert fan.capability_attributes == {} # Test set_speed not required - fan.oscillate(True) + with pytest.raises(NotImplementedError): + fan.oscillate(True) with pytest.raises(NotImplementedError): fan.set_speed("slow") with pytest.raises(NotImplementedError): From a3061ebd8db5302496ff5bef629ede4131db3f57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Nov 2020 01:10:23 -1000 Subject: [PATCH 133/430] Fix homekit bridges when no name was provided (#43364) --- homeassistant/components/homekit/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 2a0638642ed..1155d3ef18e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -146,6 +146,14 @@ RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema( ) +def _async_get_entries_by_name(current_entries): + """Return a dict of the entries by name.""" + + # For backwards compat, its possible the first bridge is using the default + # name. + return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} + + async def async_setup(hass: HomeAssistant, config: dict): """Set up the HomeKit from yaml.""" hass.data.setdefault(DOMAIN, {}) @@ -156,8 +164,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True current_entries = hass.config_entries.async_entries(DOMAIN) - - entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} + entries_by_name = _async_get_entries_by_name(current_entries) for index, conf in enumerate(config[DOMAIN]): if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): @@ -384,7 +391,7 @@ def _async_register_events_and_services(hass: HomeAssistant): return current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} + entries_by_name = _async_get_entries_by_name(current_entries) for conf in config[DOMAIN]: _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf) From 2d14f07396aabb704cb7b734c2955f1bca7985e1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 19 Nov 2020 03:26:49 -0800 Subject: [PATCH 134/430] Improve nest SDM integration error handling (#43271) --- homeassistant/components/nest/__init__.py | 18 +++- homeassistant/components/nest/camera_sdm.py | 10 +- homeassistant/components/nest/climate_sdm.py | 7 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/sensor_sdm.py | 12 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/camera_sdm_test.py | 107 ++++++------------- tests/components/nest/climate_sdm_test.py | 37 ++----- tests/components/nest/conftest.py | 57 ++++++++++ 10 files changed, 142 insertions(+), 112 deletions(-) create mode 100644 tests/components/nest/conftest.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index ea2bd549c4b..c70184c357b 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -6,6 +6,7 @@ import logging import threading from google_nest_sdm.event import EventCallback, EventMessage +from google_nest_sdm.exceptions import GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from nest import Nest from nest.nest import APIError, AuthorizationError @@ -25,6 +26,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -208,7 +210,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] ) subscriber.set_update_callback(SignalUpdateCallback(hass)) - asyncio.create_task(subscriber.start_async()) + + try: + await subscriber.start_async() + except GoogleNestException as err: + _LOGGER.error("Subscriber error: %s", err) + subscriber.stop_async() + raise ConfigEntryNotReady from err + + try: + await subscriber.async_get_device_manager() + except GoogleNestException as err: + _LOGGER.error("Device Manager error: %s", err) + subscriber.stop_async() + raise ConfigEntryNotReady from err + hass.data[DOMAIN][entry.entry_id] = subscriber for component in PLATFORMS: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index a8c5c86c4e8..bd06fb0bd8d 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,14 +4,15 @@ import datetime import logging from typing import Optional -from aiohttp.client_exceptions import ClientError from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait from google_nest_sdm.device import Device +from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import HomeAssistantType @@ -32,7 +33,10 @@ async def async_setup_sdm_entry( """Set up the cameras.""" subscriber = hass.data[DOMAIN][entry.entry_id] - device_manager = await subscriber.async_get_device_manager() + try: + device_manager = await subscriber.async_get_device_manager() + except GoogleNestException as err: + raise PlatformNotReady from err # Fetch initial data so we have data when entities subscribe. @@ -130,7 +134,7 @@ class NestCamera(Camera): self._stream_refresh_unsub = None try: self._stream = await self._stream.extend_rtsp_stream() - except ClientError as err: + except GoogleNestException as err: _LOGGER.debug("Failed to extend stream: %s", err) # Next attempt to catch a url will get a new one self._stream = None diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 57598e36ec9..f341b76c404 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -4,6 +4,7 @@ from typing import Optional from google_nest_sdm.device import Device from google_nest_sdm.device_traits import FanTrait, TemperatureTrait +from google_nest_sdm.exceptions import GoogleNestException from google_nest_sdm.thermostat_traits import ( ThermostatEcoTrait, ThermostatHvacTrait, @@ -34,6 +35,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType @@ -80,7 +82,10 @@ async def async_setup_sdm_entry( """Set up the client entities.""" subscriber = hass.data[DOMAIN][entry.entry_id] - device_manager = await subscriber.async_get_device_manager() + try: + device_manager = await subscriber.async_get_device_manager() + except GoogleNestException as err: + raise PlatformNotReady from err entities = [] for device in device_manager.devices.values(): diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 9e8a48fa95f..b994fcfbfce 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0", - "google-nest-sdm==0.1.14" + "google-nest-sdm==0.1.15" ], "codeowners": [ "@awarecan", diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 68c33529831..a0a28756ac2 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -1,9 +1,11 @@ """Support for Google Nest SDM sensors.""" +import logging from typing import Optional from google_nest_sdm.device import Device from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait +from google_nest_sdm.exceptions import GoogleNestException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -12,6 +14,7 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -19,6 +22,9 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, SIGNAL_NEST_UPDATE from .device_info import DeviceInfo +_LOGGER = logging.getLogger(__name__) + + DEVICE_TYPE_MAP = { "sdm.devices.types.CAMERA": "Camera", "sdm.devices.types.DISPLAY": "Display", @@ -33,7 +39,11 @@ async def async_setup_sdm_entry( """Set up the sensors.""" subscriber = hass.data[DOMAIN][entry.entry_id] - device_manager = await subscriber.async_get_device_manager() + try: + device_manager = await subscriber.async_get_device_manager() + except GoogleNestException as err: + _LOGGER.warning("Failed to get devices: %s", err) + raise PlatformNotReady from err # Fetch initial data so we have data when entities subscribe. diff --git a/requirements_all.txt b/requirements_all.txt index d4a35de3e97..b1d1fddbe33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -687,7 +687,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.1.14 +google-nest-sdm==0.1.15 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de236d4f0fd..53f73f346d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -358,7 +358,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.1.14 +google-nest-sdm==0.1.15 # homeassistant.components.gree greeclimate==0.10.3 diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index e3397129ec9..4a018305bcf 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -6,10 +6,8 @@ pubsub subscriber. """ import datetime -from typing import List -from aiohttp.client_exceptions import ClientConnectionError -from google_nest_sdm.auth import AbstractAuth +import aiohttp from google_nest_sdm.device import Device from homeassistant.components import camera @@ -41,47 +39,6 @@ DATETIME_FORMAT = "YY-MM-DDTHH:MM:SS" DOMAIN = "nest" -class FakeResponse: - """A fake web response used for returning results of commands.""" - - def __init__(self, json=None, error=None): - """Initialize the FakeResponse.""" - self._json = json - self._error = error - - def raise_for_status(self): - """Mimics a successful response status.""" - if self._error: - raise self._error - pass - - async def json(self): - """Return a dict with the response.""" - assert self._json - return self._json - - -class FakeAuth(AbstractAuth): - """Fake authentication object that returns fake responses.""" - - def __init__(self, responses: List[FakeResponse]): - """Initialize the FakeAuth.""" - super().__init__(None, "") - self._responses = responses - - async def async_get_access_token(self): - """Return a fake access token.""" - return "some-token" - - async def creds(self): - """Return a fake creds.""" - return None - - async def request(self, method: str, url: str, **kwargs): - """Pass through the FakeResponse.""" - return self._responses.pop(0) - - async def async_setup_camera(hass, traits={}, auth=None): """Set up the platform and prerequisites.""" devices = {} @@ -145,21 +102,25 @@ async def test_camera_device(hass): assert device.identifiers == {("nest", DEVICE_ID)} -async def test_camera_stream(hass, aiohttp_client): +async def test_camera_stream(hass, auth): """Test a basic camera and fetch its live stream.""" now = utcnow() expiration = now + datetime.timedelta(seconds=100) - response = FakeResponse( - { - "results": { - "streamUrls": {"rtspUrl": "rtsp://some/url?auth=g.0.streamingToken"}, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.0.streamingToken", - "expiresAt": expiration.isoformat(timespec="seconds"), - }, - } - ) - await async_setup_camera(hass, DEVICE_TRAITS, auth=FakeAuth([response])) + auth.responses = [ + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.0.streamingToken" + }, + "streamExtensionToken": "g.1.extensionToken", + "streamToken": "g.0.streamingToken", + "expiresAt": expiration.isoformat(timespec="seconds"), + }, + } + ) + ] + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -179,15 +140,15 @@ async def test_camera_stream(hass, aiohttp_client): assert image.content == b"image bytes" -async def test_refresh_expired_stream_token(hass, aiohttp_client): +async def test_refresh_expired_stream_token(hass, auth): """Test a camera stream expiration and refresh.""" now = utcnow() stream_1_expiration = now + datetime.timedelta(seconds=90) stream_2_expiration = now + datetime.timedelta(seconds=180) stream_3_expiration = now + datetime.timedelta(seconds=360) - responses = [ + auth.responses = [ # Stream URL #1 - FakeResponse( + aiohttp.web.json_response( { "results": { "streamUrls": { @@ -200,7 +161,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client): } ), # Stream URL #2 - FakeResponse( + aiohttp.web.json_response( { "results": { "streamExtensionToken": "g.2.extensionToken", @@ -210,7 +171,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client): } ), # Stream URL #3 - FakeResponse( + aiohttp.web.json_response( { "results": { "streamExtensionToken": "g.3.extensionToken", @@ -223,7 +184,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client): await async_setup_camera( hass, DEVICE_TRAITS, - auth=FakeAuth(responses), + auth=auth, ) assert len(hass.states.async_all()) == 1 @@ -259,12 +220,12 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client): assert stream_source == "rtsp://some/url?auth=g.3.streamingToken" -async def test_camera_removed(hass, aiohttp_client): +async def test_camera_removed(hass, auth): """Test case where entities are removed and stream tokens expired.""" now = utcnow() expiration = now + datetime.timedelta(seconds=100) - responses = [ - FakeResponse( + auth.responses = [ + aiohttp.web.json_response( { "results": { "streamUrls": { @@ -276,12 +237,12 @@ async def test_camera_removed(hass, aiohttp_client): }, } ), - FakeResponse({"results": {}}), + aiohttp.web.json_response({"results": {}}), ] await async_setup_camera( hass, DEVICE_TRAITS, - auth=FakeAuth(responses), + auth=auth, ) assert len(hass.states.async_all()) == 1 @@ -297,13 +258,13 @@ async def test_camera_removed(hass, aiohttp_client): assert len(hass.states.async_all()) == 0 -async def test_refresh_expired_stream_failure(hass, aiohttp_client): +async def test_refresh_expired_stream_failure(hass, auth): """Tests a failure when refreshing the stream.""" now = utcnow() stream_1_expiration = now + datetime.timedelta(seconds=90) stream_2_expiration = now + datetime.timedelta(seconds=180) - responses = [ - FakeResponse( + auth.responses = [ + aiohttp.web.json_response( { "results": { "streamUrls": { @@ -316,9 +277,9 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client): } ), # Extending the stream fails with arbitrary error - FakeResponse(error=ClientConnectionError()), + aiohttp.web.Response(status=500), # Next attempt to get a stream fetches a new url - FakeResponse( + aiohttp.web.json_response( { "results": { "streamUrls": { @@ -334,7 +295,7 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client): await async_setup_camera( hass, DEVICE_TRAITS, - auth=FakeAuth(responses), + auth=auth, ) assert len(hass.states.async_all()) == 1 diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index 48efd32d859..4c7ec4c0163 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -364,25 +364,8 @@ async def test_thermostat_eco_heat_only(hass): assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] -class FakeAuth: - """A fake implementation of the auth class that records requests.""" - - def __init__(self): - """Initialize FakeAuth.""" - self.method = None - self.url = None - self.json = None - - async def request(self, method, url, json): - """Capure the request arguments for tests to assert on.""" - self.method = method - self.url = url - self.json = json - - -async def test_thermostat_set_hvac_mode(hass): +async def test_thermostat_set_hvac_mode(hass, auth): """Test a thermostat changing hvac modes.""" - auth = FakeAuth() subscriber = await setup_climate( hass, { @@ -467,9 +450,8 @@ async def test_thermostat_set_hvac_mode(hass): assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT -async def test_thermostat_set_eco_preset(hass): +async def test_thermostat_set_eco_preset(hass, auth): """Test a thermostat put into eco mode.""" - auth = FakeAuth() subscriber = await setup_climate( hass, { @@ -553,9 +535,8 @@ async def test_thermostat_set_eco_preset(hass): } -async def test_thermostat_set_cool(hass): +async def test_thermostat_set_cool(hass, auth): """Test a thermostat in cool mode with a temperature change.""" - auth = FakeAuth() await setup_climate( hass, { @@ -587,9 +568,8 @@ async def test_thermostat_set_cool(hass): } -async def test_thermostat_set_heat(hass): +async def test_thermostat_set_heat(hass, auth): """Test a thermostat heating mode with a temperature change.""" - auth = FakeAuth() await setup_climate( hass, { @@ -621,9 +601,8 @@ async def test_thermostat_set_heat(hass): } -async def test_thermostat_set_heat_cool(hass): +async def test_thermostat_set_heat_cool(hass, auth): """Test a thermostat in heatcool mode with a temperature change.""" - auth = FakeAuth() await setup_climate( hass, { @@ -732,9 +711,8 @@ async def test_thermostat_fan_on(hass): assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] -async def test_thermostat_set_fan(hass): +async def test_thermostat_set_fan(hass, auth): """Test a thermostat enabling the fan.""" - auth = FakeAuth() await setup_climate( hass, { @@ -805,9 +783,8 @@ async def test_thermostat_fan_empty(hass): assert ATTR_FAN_MODES not in thermostat.attributes -async def test_thermostat_target_temp(hass): +async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" - auth = FakeAuth() subscriber = await setup_climate( hass, { diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py new file mode 100644 index 00000000000..7e183ab9c82 --- /dev/null +++ b/tests/components/nest/conftest.py @@ -0,0 +1,57 @@ +"""Common libraries for test setup.""" + +import aiohttp +from google_nest_sdm.auth import AbstractAuth +import pytest + + +class FakeAuth(AbstractAuth): + """A fake implementation of the auth class that records requests. + + This class captures the outgoing requests, and can also be used by + tests to set up fake responses. This class is registered as a response + handler for a fake aiohttp_server and can simulate successes or failures + from the API. + """ + + # Tests can set fake responses here. + responses = [] + # The last request is recorded here. + method = None + url = None + json = None + + # Set up by fixture + client = None + + def __init__(self): + """Initialize FakeAuth.""" + super().__init__(None, None) + + async def async_get_access_token(self) -> str: + """Return a valid access token.""" + return "" + + async def request(self, method, url, json): + """Capure the request arguments for tests to assert on.""" + self.method = method + self.url = url + self.json = json + return await self.client.get("/") + + async def response_handler(self, request): + """Handle fake responess for aiohttp_server.""" + if len(self.responses) > 0: + return self.responses.pop(0) + return aiohttp.web.json_response() + + +@pytest.fixture +async def auth(aiohttp_client): + """Fixture for an AbstractAuth.""" + auth = FakeAuth() + app = aiohttp.web.Application() + app.router.add_get("/", auth.response_handler) + app.router.add_post("/", auth.response_handler) + auth.client = await aiohttp_client(app) + return auth From 17e1a2a78aa99ce25c42ad1af7e41803a0601b75 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 19 Nov 2020 14:23:20 +0000 Subject: [PATCH 135/430] Update ovoenergy to v1.1.11 (#43391) --- homeassistant/components/ovo_energy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index ba9579279a9..6ec03eb19a5 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -3,6 +3,6 @@ "name": "OVO Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", - "requirements": ["ovoenergy==1.1.7"], + "requirements": ["ovoenergy==1.1.11"], "codeowners": ["@timmo001"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1d1fddbe33..9481f2517cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1073,7 +1073,7 @@ oru==0.1.11 orvibo==1.1.1 # homeassistant.components.ovo_energy -ovoenergy==1.1.7 +ovoenergy==1.1.11 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53f73f346d2..a748d6cb705 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -520,7 +520,7 @@ onvif-zeep-async==1.0.0 openerz-api==0.1.0 # homeassistant.components.ovo_energy -ovoenergy==1.1.7 +ovoenergy==1.1.11 # homeassistant.components.mqtt # homeassistant.components.shiftr From edd25ae338f312622347353340930161cfc51668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kr=C3=B3l?= Date: Thu, 19 Nov 2020 15:24:28 +0100 Subject: [PATCH 136/430] Bump wolf_smartset to 0.1.8 and handle server fetch error (#43351) --- homeassistant/components/wolflink/__init__.py | 6 +++++- homeassistant/components/wolflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 611fa7da315..d04cd7a56d4 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -4,7 +4,7 @@ import logging from httpcore import ConnectError, ConnectTimeout from wolf_smartset.token_auth import InvalidAuth -from wolf_smartset.wolf_client import WolfClient +from wolf_smartset.wolf_client import FetchFailed, WolfClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -56,6 +56,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise UpdateFailed( f"Error communicating with API: {exception}" ) from exception + except FetchFailed as exception: + raise UpdateFailed( + f"Could not fetch values from server due to: {exception}" + ) from exception except InvalidAuth as exception: raise UpdateFailed("Invalid authentication during update.") from exception diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index 633318f2f62..6d038d4fb29 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -3,6 +3,6 @@ "name": "Wolf SmartSet Service", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wolflink", - "requirements": ["wolf_smartset==0.1.6"], + "requirements": ["wolf_smartset==0.1.8"], "codeowners": ["@adamkrol93"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9481f2517cb..d93b391e7f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2286,7 +2286,7 @@ withings-api==2.1.6 wled==0.4.4 # homeassistant.components.wolflink -wolf_smartset==0.1.6 +wolf_smartset==0.1.8 # homeassistant.components.xbee xbee-helper==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a748d6cb705..f3229b536eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1100,7 +1100,7 @@ withings-api==2.1.6 wled==0.4.4 # homeassistant.components.wolflink -wolf_smartset==0.1.6 +wolf_smartset==0.1.8 # homeassistant.components.xbox xbox-webapi==2.0.8 From 5dcbb634f66aa0fbd2ac4c669c32906b8132ce0b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 19 Nov 2020 16:48:43 +0100 Subject: [PATCH 137/430] Fix selector to return the selector type (#43395) --- homeassistant/helpers/selector.py | 8 +++++--- tests/helpers/test_selector.py | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 9f049e07213..b00ae972c8e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -18,12 +18,14 @@ def validate_selector(config: Any) -> Dict: selector_type = list(config)[0] - seslector_class = SELECTORS.get(selector_type) + selector_class = SELECTORS.get(selector_type) - if seslector_class is None: + if selector_class is None: raise vol.Invalid(f"Unknown selector type {selector_type} found") - return cast(Dict, seslector_class.CONFIG_SCHEMA(config[selector_type])) + return { + selector_type: cast(Dict, selector_class.CONFIG_SCHEMA(config[selector_type])) + } class Selector: diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 2b6b5cbc9f8..51490567d8e 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -14,6 +14,12 @@ def test_invalid_base_schema(schema): selector.validate_selector(schema) +def test_validate_selector(): + """Test return is the same as input.""" + schema = {"device": {"manufacturer": "mock-manuf", "model": "mock-model"}} + assert schema == selector.validate_selector(schema) + + @pytest.mark.parametrize( "schema", ( @@ -40,5 +46,5 @@ def test_device_selector_schema(schema): ), ) def test_entity_selector_schema(schema): - """Test device selector.""" + """Test entity selector.""" selector.validate_selector({"entity": schema}) From f693c8a9fd9fc8449ec6588467006bd165048989 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 19 Nov 2020 12:22:12 -0500 Subject: [PATCH 138/430] Add twinkly integration (#42103) * Add twinkly integration * Add tests for the Twinkly integration * Update Twinkly client package to fix typo * Remove support of configuration.yaml from Twinkly integration * Add ability to unload Twinkly component from the UI * Remove dead code from Twinkly * Fix invalid error namespace in Twinkly for python 3.7 * Fix tests failing on CI * Workaround code analysis issue * Move twinkly client init out of entry setup so it can be re-used between entries * Test the twinkly component initialization * React to PR review and add few more tests --- CODEOWNERS | 1 + homeassistant/components/twinkly/__init__.py | 44 ++++ .../components/twinkly/config_flow.py | 63 +++++ homeassistant/components/twinkly/const.py | 23 ++ homeassistant/components/twinkly/light.py | 216 +++++++++++++++++ .../components/twinkly/manifest.json | 9 + homeassistant/components/twinkly/strings.json | 19 ++ .../components/twinkly/translations/en.json | 19 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/twinkly/__init__.py | 69 ++++++ tests/components/twinkly/test_config_flow.py | 60 +++++ tests/components/twinkly/test_init.py | 67 ++++++ tests/components/twinkly/test_twinkly.py | 224 ++++++++++++++++++ 15 files changed, 821 insertions(+) create mode 100644 homeassistant/components/twinkly/__init__.py create mode 100644 homeassistant/components/twinkly/config_flow.py create mode 100644 homeassistant/components/twinkly/const.py create mode 100644 homeassistant/components/twinkly/light.py create mode 100644 homeassistant/components/twinkly/manifest.json create mode 100644 homeassistant/components/twinkly/strings.json create mode 100644 homeassistant/components/twinkly/translations/en.json create mode 100644 tests/components/twinkly/__init__.py create mode 100644 tests/components/twinkly/test_config_flow.py create mode 100644 tests/components/twinkly/test_init.py create mode 100644 tests/components/twinkly/test_twinkly.py diff --git a/CODEOWNERS b/CODEOWNERS index 215967c1c18..7465c42a272 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -471,6 +471,7 @@ homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli homeassistant/components/tuya/* @ollo69 homeassistant/components/twentemilieu/* @frenck +homeassistant/components/twinkly/* @dr1rrb homeassistant/components/ubee/* @mzdrale homeassistant/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py new file mode 100644 index 00000000000..2b605104609 --- /dev/null +++ b/homeassistant/components/twinkly/__init__.py @@ -0,0 +1,44 @@ +"""The twinkly component.""" + +import twinkly_client + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType + +from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN + + +async def async_setup(hass: HomeAssistantType, config: dict): + """Set up the twinkly integration.""" + + return True + + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): + """Set up entries from config flow.""" + + # We setup the client here so if at some point we add any other entity for this device, + # we will be able to properly share the connection. + uuid = config_entry.data[CONF_ENTRY_ID] + host = config_entry.data[CONF_ENTRY_HOST] + + hass.data.setdefault(DOMAIN, {})[uuid] = twinkly_client.TwinklyClient( + host, async_get_clientsession(hass) + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "light") + ) + return True + + +async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): + """Remove a twinkly entry.""" + + # For now light entries don't have unload method, so we don't have to async_forward_entry_unload + # However we still have to cleanup the shared client! + uuid = config_entry.data[CONF_ENTRY_ID] + hass.data[DOMAIN].pop(uuid) + + return True diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py new file mode 100644 index 00000000000..f1593de5643 --- /dev/null +++ b/homeassistant/components/twinkly/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow to configure the Twinkly integration.""" + +import asyncio +import logging + +from aiohttp import ClientError +import twinkly_client +from voluptuous import Required, Schema + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST + +from .const import ( + CONF_ENTRY_HOST, + CONF_ENTRY_ID, + CONF_ENTRY_MODEL, + CONF_ENTRY_NAME, + DEV_ID, + DEV_MODEL, + DEV_NAME, +) + +# https://github.com/PyCQA/pylint/issues/3202 +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle twinkly config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle config steps.""" + host = user_input[CONF_HOST] if user_input else None + + schema = {Required(CONF_HOST, default=host): str} + errors = {} + + if host is not None: + try: + device_info = await twinkly_client.TwinklyClient(host).get_device_info() + + await self.async_set_unique_id(device_info[DEV_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=device_info[DEV_NAME], + data={ + CONF_ENTRY_HOST: host, + CONF_ENTRY_ID: device_info[DEV_ID], + CONF_ENTRY_NAME: device_info[DEV_NAME], + CONF_ENTRY_MODEL: device_info[DEV_MODEL], + }, + ) + except (asyncio.TimeoutError, ClientError) as err: + _LOGGER.info("Cannot reach Twinkly '%s' (client)", host, exc_info=err) + errors[CONF_HOST] = "cannot_connect" + + return self.async_show_form( + step_id="user", data_schema=Schema(schema), errors=errors + ) diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py new file mode 100644 index 00000000000..2e7d4d2fd0e --- /dev/null +++ b/homeassistant/components/twinkly/const.py @@ -0,0 +1,23 @@ +"""Const for Twinkly.""" + +DOMAIN = "twinkly" + +# Keys of the config entry +CONF_ENTRY_ID = "id" +CONF_ENTRY_HOST = "host" +CONF_ENTRY_NAME = "name" +CONF_ENTRY_MODEL = "model" + +# Strongly named HA attributes keys +ATTR_HOST = "host" + +# Keys of attributes read from the get_device_info +DEV_ID = "uuid" +DEV_NAME = "device_name" +DEV_MODEL = "product_code" + +HIDDEN_DEV_VALUES = ( + "code", # This is the internal status code of the API response + "copyright", # We should not display a copyright "LEDWORKS 2018" in the Home-Assistant UI + "mac", # Does not report the actual device mac address +) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py new file mode 100644 index 00000000000..8de51d19d51 --- /dev/null +++ b/homeassistant/components/twinkly/light.py @@ -0,0 +1,216 @@ +"""The Twinkly light component.""" + +import asyncio +import logging +from typing import Any, Dict, Optional + +from aiohttp import ClientError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_HOST, + CONF_ENTRY_HOST, + CONF_ENTRY_ID, + CONF_ENTRY_MODEL, + CONF_ENTRY_NAME, + DEV_MODEL, + DEV_NAME, + DOMAIN, + HIDDEN_DEV_VALUES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Setups an entity from a config entry (UI config flow).""" + + entity = TwinklyLight(config_entry, hass) + + async_add_entities([entity], update_before_add=True) + + +class TwinklyLight(LightEntity): + """Implementation of the light for the Twinkly service.""" + + def __init__( + self, + conf: ConfigEntry, + hass: HomeAssistantType, + ): + """Initialize a TwinklyLight entity.""" + self._id = conf.data[CONF_ENTRY_ID] + self._hass = hass + self._conf = conf + + # Those are saved in the config entry in order to have meaningful values even + # if the device is currently offline. + # They are expected to be updated using the device_info. + self.__name = conf.data[CONF_ENTRY_NAME] + self.__model = conf.data[CONF_ENTRY_MODEL] + + self._client = hass.data.get(DOMAIN, {}).get(self._id) + if self._client is None: + raise ValueError(f"Client for {self._id} has not been configured.") + + # Set default state before any update + self._is_on = False + self._brightness = 0 + self._is_available = False + self._attributes = {ATTR_HOST: self._client.host} + + @property + def supported_features(self): + """Get the features supported by this entity.""" + return SUPPORT_BRIGHTNESS + + @property + def should_poll(self) -> bool: + """Get a boolean which indicates if this entity should be polled.""" + return True + + @property + def available(self) -> bool: + """Get a boolean which indicates if this entity is currently available.""" + return self._is_available + + @property + def unique_id(self) -> Optional[str]: + """Id of the device.""" + return self._id + + @property + def name(self) -> str: + """Name of the device.""" + return self.__name if self.__name else "Twinkly light" + + @property + def model(self) -> str: + """Name of the device.""" + return self.__model + + @property + def icon(self) -> str: + """Icon of the device.""" + return "mdi:string-lights" + + @property + def device_info(self) -> Optional[Dict[str, Any]]: + """Get device specific attributes.""" + return ( + { + "identifiers": {(DOMAIN, self._id)}, + "name": self.name, + "manufacturer": "LEDWORKS", + "model": self.model, + } + if self._id + else None # device_info is available only for entities configured from the UI + ) + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._is_on + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of the light.""" + return self._brightness + + @property + def state_attributes(self) -> dict: + """Return device specific state attributes.""" + + attributes = self._attributes + + # Make sure to update any normalized property + attributes[ATTR_HOST] = self._client.host + attributes[ATTR_BRIGHTNESS] = self._brightness + + return attributes + + async def async_turn_on(self, **kwargs) -> None: + """Turn device on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = int(int(kwargs[ATTR_BRIGHTNESS]) / 2.55) + + # If brightness is 0, the twinkly will only "disable" the brightness, + # which means that it will be 100%. + if brightness == 0: + await self._client.set_is_on(False) + return + + await self._client.set_brightness(brightness) + + await self._client.set_is_on(True) + + async def async_turn_off(self, **kwargs) -> None: + """Turn device off.""" + await self._client.set_is_on(False) + + async def async_update(self) -> None: + """Asynchronously updates the device properties.""" + _LOGGER.info("Updating '%s'", self._client.host) + + try: + self._is_on = await self._client.get_is_on() + + self._brightness = ( + int(round((await self._client.get_brightness()) * 2.55)) + if self._is_on + else 0 + ) + + device_info = await self._client.get_device_info() + + if ( + DEV_NAME in device_info + and DEV_MODEL in device_info + and ( + device_info[DEV_NAME] != self.__name + or device_info[DEV_MODEL] != self.__model + ) + ): + self.__name = device_info[DEV_NAME] + self.__model = device_info[DEV_MODEL] + + if self._conf is not None: + # If the name has changed, persist it in conf entry, + # so we will be able to restore this new name if hass is started while the LED string is offline. + self._hass.config_entries.async_update_entry( + self._conf, + data={ + CONF_ENTRY_HOST: self._client.host, # this cannot change + CONF_ENTRY_ID: self._id, # this cannot change + CONF_ENTRY_NAME: self.__name, + CONF_ENTRY_MODEL: self.__model, + }, + ) + + for key, value in device_info.items(): + if key not in HIDDEN_DEV_VALUES: + self._attributes[key] = value + + if not self._is_available: + _LOGGER.info("Twinkly '%s' is now available", self._client.host) + + # We don't use the echo API to track the availability since we already have to pull + # the device to get its state. + self._is_available = True + except (asyncio.TimeoutError, ClientError): + # We log this as "info" as it's pretty common that the christmas light are not reachable in july + if self._is_available: + _LOGGER.info( + "Twinkly '%s' is not reachable (client error)", self._client.host + ) + self._is_available = False diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json new file mode 100644 index 00000000000..c87394ba3bb --- /dev/null +++ b/homeassistant/components/twinkly/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "twinkly", + "name": "Twinkly", + "documentation": "https://www.home-assistant.io/integrations/twinkly", + "requirements": ["twinkly-client==0.0.2"], + "dependencies": [], + "codeowners": ["@dr1rrb"], + "config_flow": true +} diff --git a/homeassistant/components/twinkly/strings.json b/homeassistant/components/twinkly/strings.json new file mode 100644 index 00000000000..70e7f970b58 --- /dev/null +++ b/homeassistant/components/twinkly/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "title": "Twinkly", + "description": "Set up your Twinkly led string", + "data": { + "host": "Host (or IP address) of your twinkly device" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "device_exists": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/twinkly/translations/en.json b/homeassistant/components/twinkly/translations/en.json new file mode 100644 index 00000000000..2126bac3c27 --- /dev/null +++ b/homeassistant/components/twinkly/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host (or IP address) of your twinkly device" + }, + "description": "Set up your Twinkly led string", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d7cd4fd20ba..5386ff4ce60 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -208,6 +208,7 @@ FLOWS = [ "tuya", "twentemilieu", "twilio", + "twinkly", "unifi", "upb", "upcloud", diff --git a/requirements_all.txt b/requirements_all.txt index d93b391e7f9..b64e4184785 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2209,6 +2209,9 @@ twentemilieu==0.3.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twinkly +twinkly-client==0.0.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3229b536eb..b0691062b53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1062,6 +1062,9 @@ twentemilieu==0.3.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twinkly +twinkly-client==0.0.2 + # homeassistant.components.upb upb_lib==0.4.11 diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py new file mode 100644 index 00000000000..96f9f450b8a --- /dev/null +++ b/tests/components/twinkly/__init__.py @@ -0,0 +1,69 @@ +"""Constants and mock for the twkinly component tests.""" + +from uuid import uuid4 + +from aiohttp.client_exceptions import ClientConnectionError + +from homeassistant.components.twinkly.const import DEV_NAME + +TEST_HOST = "test.twinkly.com" +TEST_ID = "twinkly_test_device_id" +TEST_NAME = "twinkly_test_device_name" +TEST_NAME_ORIGINAL = "twinkly_test_original_device_name" # the original (deprecated) name stored in the conf +TEST_MODEL = "twinkly_test_device_model" + + +class ClientMock: + """A mock of the twinkly_client.TwinklyClient.""" + + def __init__(self) -> None: + """Create a mocked client.""" + self.is_offline = False + self.is_on = True + self.brightness = 10 + + self.id = str(uuid4()) + self.device_info = { + "uuid": self.id, + "device_name": self.id, # we make sure that entity id is different for each test + "product_code": TEST_MODEL, + } + + @property + def host(self) -> str: + """Get the mocked host.""" + return TEST_HOST + + async def get_device_info(self): + """Get the mocked device info.""" + if self.is_offline: + raise ClientConnectionError() + return self.device_info + + async def get_is_on(self) -> bool: + """Get the mocked on/off state.""" + if self.is_offline: + raise ClientConnectionError() + return self.is_on + + async def set_is_on(self, is_on: bool) -> None: + """Set the mocked on/off state.""" + if self.is_offline: + raise ClientConnectionError() + self.is_on = is_on + + async def get_brightness(self) -> int: + """Get the mocked brightness.""" + if self.is_offline: + raise ClientConnectionError() + return self.brightness + + async def set_brightness(self, brightness: int) -> None: + """Set the mocked brightness.""" + if self.is_offline: + raise ClientConnectionError() + self.brightness = brightness + + def change_name(self, new_name: str) -> None: + """Change the name of this virtual device.""" + self.device_info[DEV_NAME] = new_name diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py new file mode 100644 index 00000000000..d1a56277fa7 --- /dev/null +++ b/tests/components/twinkly/test_config_flow.py @@ -0,0 +1,60 @@ +"""Tests for the config_flow of the twinly component.""" + +from homeassistant import config_entries +from homeassistant.components.twinkly.const import ( + CONF_ENTRY_HOST, + CONF_ENTRY_ID, + CONF_ENTRY_MODEL, + CONF_ENTRY_NAME, + DOMAIN as TWINKLY_DOMAIN, +) + +from tests.async_mock import patch +from tests.components.twinkly import TEST_MODEL, ClientMock + + +async def test_invalid_host(hass): + """Test the failure when invalid host provided.""" + result = await hass.config_entries.flow.async_init( + TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ENTRY_HOST: "dummy"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {CONF_ENTRY_HOST: "cannot_connect"} + + +async def test_success_flow(hass): + """Test that an entity is created when the flow completes.""" + client = ClientMock() + with patch("twinkly_client.TwinklyClient", return_value=client): + result = await hass.config_entries.flow.async_init( + TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ENTRY_HOST: "dummy"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == client.id + assert result["data"] == { + CONF_ENTRY_HOST: "dummy", + CONF_ENTRY_ID: client.id, + CONF_ENTRY_NAME: client.id, + CONF_ENTRY_MODEL: TEST_MODEL, + } diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py new file mode 100644 index 00000000000..d9dc4623d5e --- /dev/null +++ b/tests/components/twinkly/test_init.py @@ -0,0 +1,67 @@ +"""Tests of the initialization of the twinly integration.""" + +from uuid import uuid4 + +from homeassistant.components.twinkly import async_setup_entry, async_unload_entry +from homeassistant.components.twinkly.const import ( + CONF_ENTRY_HOST, + CONF_ENTRY_ID, + CONF_ENTRY_MODEL, + CONF_ENTRY_NAME, + DOMAIN as TWINKLY_DOMAIN, +) +from homeassistant.core import HomeAssistant + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.twinkly import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL + + +async def test_setup_entry(hass: HomeAssistant): + """Validate that setup entry also configure the client.""" + + id = str(uuid4()) + config_entry = MockConfigEntry( + domain=TWINKLY_DOMAIN, + data={ + CONF_ENTRY_HOST: TEST_HOST, + CONF_ENTRY_ID: id, + CONF_ENTRY_NAME: TEST_NAME_ORIGINAL, + CONF_ENTRY_MODEL: TEST_MODEL, + }, + entry_id=id, + ) + + def setup_mock(_, __): + return True + + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + side_effect=setup_mock, + ): + await async_setup_entry(hass, config_entry) + + assert hass.data[TWINKLY_DOMAIN][id] is not None + + +async def test_unload_entry(hass: HomeAssistant): + """Validate that unload entry also clear the client.""" + + id = str(uuid4()) + config_entry = MockConfigEntry( + domain=TWINKLY_DOMAIN, + data={ + CONF_ENTRY_HOST: TEST_HOST, + CONF_ENTRY_ID: id, + CONF_ENTRY_NAME: TEST_NAME_ORIGINAL, + CONF_ENTRY_MODEL: TEST_MODEL, + }, + entry_id=id, + ) + + # Put random content at the location where the client should have been placed by setup + hass.data.setdefault(TWINKLY_DOMAIN, {})[id] = config_entry + + await async_unload_entry(hass, config_entry) + + assert hass.data[TWINKLY_DOMAIN].get(id) is None diff --git a/tests/components/twinkly/test_twinkly.py b/tests/components/twinkly/test_twinkly.py new file mode 100644 index 00000000000..7f73589512a --- /dev/null +++ b/tests/components/twinkly/test_twinkly.py @@ -0,0 +1,224 @@ +"""Tests for the integration of a twinly device.""" + +from typing import Tuple + +from homeassistant.components.twinkly.const import ( + CONF_ENTRY_HOST, + CONF_ENTRY_ID, + CONF_ENTRY_MODEL, + CONF_ENTRY_NAME, + DOMAIN as TWINKLY_DOMAIN, +) +from homeassistant.components.twinkly.light import TwinklyLight +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import RegistryEntry + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.twinkly import ( + TEST_HOST, + TEST_ID, + TEST_MODEL, + TEST_NAME_ORIGINAL, + ClientMock, +) + + +async def test_missing_client(hass: HomeAssistant): + """Validate that if client has not been setup, it fails immediately in setup.""" + try: + config_entry = MockConfigEntry( + data={ + CONF_ENTRY_HOST: TEST_HOST, + CONF_ENTRY_ID: TEST_ID, + CONF_ENTRY_NAME: TEST_NAME_ORIGINAL, + CONF_ENTRY_MODEL: TEST_MODEL, + } + ) + TwinklyLight(config_entry, hass) + except ValueError: + return + + assert False + + +async def test_initial_state(hass: HomeAssistant): + """Validate that entity and device states are updated on startup.""" + entity, device, _ = await _create_entries(hass) + + state = hass.states.get(entity.entity_id) + + # Basic state properties + assert state.name == entity.unique_id + assert state.state == "on" + assert state.attributes["host"] == TEST_HOST + assert state.attributes["brightness"] == 26 + assert state.attributes["friendly_name"] == entity.unique_id + assert state.attributes["icon"] == "mdi:string-lights" + + # Validates that custom properties of the API device_info are propagated through attributes + assert state.attributes["uuid"] == entity.unique_id + + assert entity.original_name == entity.unique_id + assert entity.original_icon == "mdi:string-lights" + + assert device.name == entity.unique_id + assert device.model == TEST_MODEL + assert device.manufacturer == "LEDWORKS" + + +async def test_initial_state_offline(hass: HomeAssistant): + """Validate that entity and device are restored from config is offline on startup.""" + client = ClientMock() + client.is_offline = True + entity, device, _ = await _create_entries(hass, client) + + state = hass.states.get(entity.entity_id) + + assert state.name == TEST_NAME_ORIGINAL + assert state.state == "unavailable" + assert state.attributes["friendly_name"] == TEST_NAME_ORIGINAL + assert state.attributes["icon"] == "mdi:string-lights" + + assert entity.original_name == TEST_NAME_ORIGINAL + assert entity.original_icon == "mdi:string-lights" + + assert device.name == TEST_NAME_ORIGINAL + assert device.model == TEST_MODEL + assert device.manufacturer == "LEDWORKS" + + +async def test_turn_on(hass: HomeAssistant): + """Test support of the light.turn_on service.""" + client = ClientMock() + client.is_on = False + client.brightness = 20 + entity, _, _ = await _create_entries(hass, client) + + assert hass.states.get(entity.entity_id).state == "off" + + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity.entity_id} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + + assert state.state == "on" + assert state.attributes["brightness"] == 51 + + +async def test_turn_on_with_brightness(hass: HomeAssistant): + """Test support of the light.turn_on service with a brightness parameter.""" + client = ClientMock() + client.is_on = False + client.brightness = 20 + entity, _, _ = await _create_entries(hass, client) + + assert hass.states.get(entity.entity_id).state == "off" + + await hass.services.async_call( + "light", + "turn_on", + service_data={"entity_id": entity.entity_id, "brightness": 255}, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + + assert state.state == "on" + assert state.attributes["brightness"] == 255 + + +async def test_turn_off(hass: HomeAssistant): + """Test support of the light.turn_off service.""" + entity, _, _ = await _create_entries(hass) + + assert hass.states.get(entity.entity_id).state == "on" + + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity.entity_id} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + + assert state.state == "off" + assert state.attributes["brightness"] == 0 + + +async def test_update_name(hass: HomeAssistant): + """ + Validate device's name update behavior. + + Validate that if device name is changed from the Twinkly app, + then the name of the entity is updated and it's also persisted, + so it can be restored when starting HA while Twinkly is offline. + """ + entity, _, client = await _create_entries(hass) + + updated_config_entry = None + + async def on_update(ha, co): + nonlocal updated_config_entry + updated_config_entry = co + + hass.config_entries.async_get_entry(entity.unique_id).add_update_listener(on_update) + + client.change_name("new_device_name") + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity.entity_id} + ) # We call turn_off which will automatically cause an async_update + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + + assert updated_config_entry is not None + assert updated_config_entry.data[CONF_ENTRY_NAME] == "new_device_name" + assert state.attributes["friendly_name"] == "new_device_name" + + +async def test_unload(hass: HomeAssistant): + """Validate that entities can be unloaded from the UI.""" + + _, _, client = await _create_entries(hass) + entry_id = client.id + + assert await hass.config_entries.async_unload(entry_id) + + +async def _create_entries( + hass: HomeAssistant, client=None +) -> Tuple[RegistryEntry, DeviceEntry, ClientMock]: + client = ClientMock() if client is None else client + + def get_client_mock(client, _): + return client + + with patch("twinkly_client.TwinklyClient", side_effect=get_client_mock): + config_entry = MockConfigEntry( + domain=TWINKLY_DOMAIN, + data={ + CONF_ENTRY_HOST: client, + CONF_ENTRY_ID: client.id, + CONF_ENTRY_NAME: TEST_NAME_ORIGINAL, + CONF_ENTRY_MODEL: TEST_MODEL, + }, + entry_id=client.id, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(client.id) + await hass.async_block_till_done() + + device_registry = await hass.helpers.device_registry.async_get_registry() + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id) + entity = entity_registry.async_get(entity_id) + device = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}, set()) + + assert entity is not None + assert device is not None + + return entity, device, client From d3f952f83186ece26e6957d85f41ccb14bbc9e14 Mon Sep 17 00:00:00 2001 From: Anton Palgunov Date: Thu, 19 Nov 2020 20:56:53 +0000 Subject: [PATCH 139/430] Add new air-humidifier device CA4 with miot protocol (#39398) * Add new air-humidifier device CA4 with miot protocol * Update homeassistant/components/xiaomi_miio/fan.py Co-authored-by: Teemu R. * xiomi_miio fan Standard speed for fan, removed static attributes * xiomi_miio fan Standard speed for fan, removed static attributes * xiomi_miio fan unnessary elif * added mode_mapping * Changed mode_mapping to constant also move reverse to constant * Removerd water_level * Return to exclude breaking changes Co-authored-by: Teemu R. --- homeassistant/components/xiaomi_miio/const.py | 1 + homeassistant/components/xiaomi_miio/fan.py | 128 +++++++++++++++++- .../components/xiaomi_miio/services.yaml | 10 ++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 3effe4975ea..8de68cda97f 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -21,6 +21,7 @@ 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" +SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed" # Light Services SERVICE_SET_SCENE = "light_set_scene" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 8d971446a53..e2a1b3b8143 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -7,6 +7,7 @@ import logging from miio import ( # pylint: disable=import-error AirFresh, AirHumidifier, + AirHumidifierMiot, AirPurifier, AirPurifierMiot, Device, @@ -20,6 +21,11 @@ from miio.airhumidifier import ( # pylint: disable=import-error, import-error LedBrightness as AirhumidifierLedBrightness, OperationMode as AirhumidifierOperationMode, ) +from miio.airhumidifier_miot import ( # pylint: disable=import-error, import-error + LedBrightness as AirhumidifierMiotLedBrightness, + OperationMode as AirhumidifierMiotOperationMode, + PressedButton as AirhumidifierPressedButton, +) from miio.airpurifier import ( # pylint: disable=import-error, import-error LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, @@ -30,7 +36,14 @@ from miio.airpurifier_miot import ( # pylint: disable=import-error, import-erro ) import voluptuous as vol -from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity +from homeassistant.components.fan import ( + PLATFORM_SCHEMA, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SUPPORT_SET_SPEED, + FanEntity, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -60,6 +73,7 @@ from .const import ( SERVICE_SET_LEARN_MODE_OFF, SERVICE_SET_LEARN_MODE_ON, SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_MOTOR_SPEED, SERVICE_SET_TARGET_HUMIDITY, SERVICE_SET_VOLUME, ) @@ -88,6 +102,7 @@ MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4" MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1" MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" @@ -116,6 +131,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( MODEL_AIRPURIFIER_3H, MODEL_AIRHUMIDIFIER_V1, MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRFRESH_VA2, ] @@ -169,10 +185,16 @@ ATTR_HARDWARE_VERSION = "hardware_version" ATTR_DEPTH = "depth" ATTR_DRY = "dry" +# Air Humidifier CA4 +ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" +ATTR_FAHRENHEIT = "fahrenheit" +ATTR_FAULT = "fault" + # Air Fresh ATTR_CO2 = "co2" PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H] +HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4] # Map attributes to properties of the state object AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { @@ -299,13 +321,13 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON = { ATTR_TARGET_HUMIDITY: "target_humidity", ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_USE_TIME: "use_time", - ATTR_HARDWARE_VERSION: "hardware_version", } AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, ATTR_TRANS_LEVEL: "trans_level", ATTR_BUTTON_PRESSED: "button_pressed", + ATTR_HARDWARE_VERSION: "hardware_version", } AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = { @@ -313,6 +335,16 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = { ATTR_MOTOR_SPEED: "motor_speed", ATTR_DEPTH: "depth", ATTR_DRY: "dry", + ATTR_HARDWARE_VERSION: "hardware_version", +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 = { + **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, + ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", + ATTR_BUTTON_PRESSED: "button_pressed", + ATTR_DRY: "dry", + ATTR_FAHRENHEIT: "fahrenheit", + ATTR_MOTOR_SPEED: "motor_speed", } AVAILABLE_ATTRIBUTES_AIRFRESH = { @@ -364,6 +396,7 @@ FEATURE_SET_EXTRA_FEATURES = 512 FEATURE_SET_TARGET_HUMIDITY = 1024 FEATURE_SET_DRY = 2048 FEATURE_SET_FAN_LEVEL = 4096 +FEATURE_SET_MOTOR_SPEED = 8192 FEATURE_FLAGS_AIRPURIFIER = ( FEATURE_SET_BUZZER @@ -421,6 +454,15 @@ FEATURE_FLAGS_AIRHUMIDIFIER = ( FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY +FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_SET_TARGET_HUMIDITY + | FEATURE_SET_DRY + | FEATURE_SET_MOTOR_SPEED +) + FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK @@ -460,6 +502,14 @@ SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend( } ) +SERVICE_SCHEMA_MOTOR_SPEED = AIRPURIFIER_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_MOTOR_SPEED): vol.All( + vol.Coerce(int), vol.Clamp(min=200, max=2000) + ) + } +) + SERVICE_TO_METHOD = { SERVICE_SET_BUZZER_ON: {"method": "async_set_buzzer_on"}, SERVICE_SET_BUZZER_OFF: {"method": "async_set_buzzer_off"}, @@ -495,6 +545,10 @@ SERVICE_TO_METHOD = { }, SERVICE_SET_DRY_ON: {"method": "async_set_dry_on"}, SERVICE_SET_DRY_OFF: {"method": "async_set_dry_off"}, + SERVICE_SET_MOTOR_SPEED: { + "method": "async_set_motor_speed", + "schema": SERVICE_SCHEMA_MOTOR_SPEED, + }, } @@ -532,6 +586,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= elif model.startswith("zhimi.airpurifier."): air_purifier = AirPurifier(host, token) device = XiaomiAirPurifier(name, air_purifier, model, unique_id) + elif model in HUMIDIFIER_MIOT: + air_humidifier = AirHumidifierMiot(host, token) + device = XiaomiAirHumidifierMiot(name, air_humidifier, model, unique_id) elif model.startswith("zhimi.humidifier."): air_humidifier = AirHumidifier(host, token, model=model) device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) @@ -998,6 +1055,10 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): for mode in AirhumidifierOperationMode if mode is not AirhumidifierOperationMode.Strong ] + elif self._model in [MODEL_AIRHUMIDIFIER_CA4]: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 + self._speed_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] else: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER @@ -1107,6 +1168,69 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): ) +class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): + """Representation of a Xiaomi Air Humidifier (MiOT protocol).""" + + MODE_MAPPING = { + AirhumidifierMiotOperationMode.Low: SPEED_LOW, + AirhumidifierMiotOperationMode.Mid: SPEED_MEDIUM, + AirhumidifierMiotOperationMode.High: SPEED_HIGH, + } + + REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()} + + @property + def speed(self): + """Return the current speed.""" + if self._state: + return self.MODE_MAPPING.get( + AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) + ) + + return None + + @property + def button_pressed(self): + """Return the last button pressed.""" + if self._state: + return AirhumidifierPressedButton( + self._state_attrs[ATTR_BUTTON_PRESSED] + ).name + + return None + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.REVERSE_MODE_MAPPING[speed], + ) + + async def async_set_led_brightness(self, brightness: int = 2): + """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirhumidifierMiotLedBrightness(brightness), + ) + + async def async_set_motor_speed(self, motor_speed: int = 400): + """Set the target motor speed.""" + if self._device_features & FEATURE_SET_MOTOR_SPEED == 0: + return + + await self._try_command( + "Setting the target motor speed of the miio device failed.", + self._device.set_speed, + motor_speed, + ) + + class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index c61b7f37f22..f0312f01991 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -149,6 +149,16 @@ fan_set_dry_off: description: Name of the xiaomi miio entity. example: "fan.xiaomi_miio_device" +fan_set_motor_speed: + description: Set the target motor speed. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + motor_speed: + description: Set RPM of motor speed, between 200 and 2000. + example: 1100 + light_set_scene: description: Set a fixed scene. fields: From 390668e192b99b111e4c135a54989db30d32e875 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Nov 2020 22:05:36 +0100 Subject: [PATCH 140/430] Check config to use config platforms (#43407) --- homeassistant/helpers/check_config.py | 26 +++++++++++++++++++++ tests/helpers/test_check_config.py | 33 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 7b71b7aae2b..018bbe5cfe0 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -123,6 +123,32 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig result.add_error(f"Component error: {domain} - {ex}") continue + # Check if the integration has a custom config validator + config_validator = None + try: + config_validator = integration.get_platform("config") + except ImportError as err: + # Filter out import error of the config platform. + # If the config platform contains bad imports, make sure + # that still fails. + if err.name != f"{integration.pkg_path}.config": + result.add_error(f"Error importing config platform {domain}: {err}") + continue + + 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: + _comp_error(ex, domain, config) + continue + except Exception: # pylint: disable=broad-except + result.add_error("Unknown error calling %s config validator", domain) + continue + config_schema = getattr(component, "CONFIG_SCHEMA", None) if config_schema is not None: try: diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index ffc05544694..786fa986a14 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -133,3 +133,36 @@ async def test_bootstrap_error(hass, loop): # Only 1 error expected res.errors.pop(0) assert not res.errors + + +async def test_automation_config_platform(hass): + """Test automation async config.""" + files = { + YAML_CONFIG_FILE: BASE_CONFIG + + """ +automation: + use_blueprint: + path: test_event_service.yaml + input: + trigger_event: blueprint_event + service_to_call: test.automation +""", + hass.config.path( + "blueprints/automation/test_event_service.yaml" + ): """ +blueprint: + name: "Call service based on event" + domain: automation + input: + trigger_event: + service_to_call: +trigger: + platform: event + event_type: !placeholder trigger_event +action: + service: !placeholder service_to_call +""", + } + with patch("os.path.isfile", return_value=True), patch_yaml_files(files): + res = await async_check_ha_config_file(hass) + assert len(res["automation"]) == 1 From c255e744635a2aeaa486f2b16c78c380c401cc5c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 20 Nov 2020 00:06:26 +0000 Subject: [PATCH 141/430] [ci skip] Translation update --- .../binary_sensor/translations/ru.json | 8 +++--- .../binary_sensor/translations/zh-Hans.json | 16 +++++++++++ .../cloud/translations/zh-Hans.json | 16 +++++++++++ .../components/cover/translations/it.json | 28 +++++++++---------- .../components/hassio/translations/cs.json | 2 +- .../components/hassio/translations/es.json | 4 +-- .../hassio/translations/zh-Hans.json | 11 ++++++++ .../homeassistant/translations/zh-Hans.json | 21 ++++++++++++++ .../components/homekit/translations/ko.json | 2 +- .../components/lovelace/translations/cs.json | 2 +- .../lovelace/translations/zh-Hans.json | 10 +++++++ .../components/rpi_power/translations/ko.json | 2 +- .../components/twinkly/translations/cs.json | 18 ++++++++++++ .../components/twinkly/translations/et.json | 19 +++++++++++++ .../components/twinkly/translations/ru.json | 19 +++++++++++++ .../components/vizio/translations/it.json | 1 + .../components/vizio/translations/ru.json | 1 + 17 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/cloud/translations/zh-Hans.json create mode 100644 homeassistant/components/homeassistant/translations/zh-Hans.json create mode 100644 homeassistant/components/lovelace/translations/zh-Hans.json create mode 100644 homeassistant/components/twinkly/translations/cs.json create mode 100644 homeassistant/components/twinkly/translations/et.json create mode 100644 homeassistant/components/twinkly/translations/ru.json diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 6cb5e4a8b56..6d2dd417a94 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -8,7 +8,7 @@ "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", + "is_moist": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \"\u0412\u043b\u0430\u0436\u043d\u043e\"", "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", @@ -23,7 +23,7 @@ "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_moist": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \"\u0421\u0443\u0445\u043e\"", "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", @@ -52,7 +52,7 @@ "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": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0432\u043b\u0430\u0436\u043d\u044b\u043c", "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", @@ -67,7 +67,7 @@ "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_moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0441\u0443\u0445\u0438\u043c", "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", diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json index fe16bd685ca..9254f667a48 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -71,6 +71,10 @@ "off": "\u6b63\u5e38", "on": "\u4f4e" }, + "battery_charging": { + "off": "\u672a\u5728\u5145\u7535", + "on": "\u6b63\u5728\u5145\u7535" + }, "cold": { "off": "\u6b63\u5e38", "on": "\u8fc7\u51b7" @@ -95,6 +99,10 @@ "off": "\u6b63\u5e38", "on": "\u8fc7\u70ed" }, + "light": { + "off": "\u6ca1\u6709\u5149\u7ebf", + "on": "\u68c0\u6d4b\u5230\u5149\u7ebf" + }, "lock": { "off": "\u4e0a\u9501", "on": "\u89e3\u9501" @@ -107,6 +115,10 @@ "off": "\u672a\u89e6\u53d1", "on": "\u89e6\u53d1" }, + "moving": { + "off": "\u9759\u6b62", + "on": "\u6b63\u5728\u79fb\u52a8" + }, "occupancy": { "off": "\u672a\u89e6\u53d1", "on": "\u5df2\u89e6\u53d1" @@ -115,6 +127,10 @@ "off": "\u5173\u95ed", "on": "\u5f00\u542f" }, + "plug": { + "off": "\u5df2\u62d4\u51fa", + "on": "\u5df2\u63d2\u5165" + }, "presence": { "off": "\u79bb\u5f00", "on": "\u5728\u5bb6" diff --git a/homeassistant/components/cloud/translations/zh-Hans.json b/homeassistant/components/cloud/translations/zh-Hans.json new file mode 100644 index 00000000000..eb1daf5e4f3 --- /dev/null +++ b/homeassistant/components/cloud/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "\u5df2\u542f\u7528 Alexa", + "can_reach_cert_server": "\u53ef\u8bbf\u95ee\u8bc1\u4e66\u670d\u52a1\u5668", + "can_reach_cloud": "\u53ef\u8bbf\u95ee Home Assistant Cloud", + "can_reach_cloud_auth": "\u53ef\u8bbf\u95ee\u8ba4\u8bc1\u670d\u52a1\u5668", + "google_enabled": "\u5df2\u542f\u7528 Google", + "logged_in": "\u5df2\u767b\u5f55", + "relayer_connected": "\u901a\u8fc7\u4ee3\u7406\u8fde\u63a5", + "remote_connected": "\u8fdc\u7a0b\u8fde\u63a5", + "remote_enabled": "\u5df2\u542f\u7528\u8fdc\u7a0b\u63a7\u5236", + "subscription_expiration": "\u8ba2\u9605\u5230\u671f\u65f6\u95f4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/it.json b/homeassistant/components/cover/translations/it.json index 90322b9f122..dbcb6425238 100644 --- a/homeassistant/components/cover/translations/it.json +++ b/homeassistant/components/cover/translations/it.json @@ -5,35 +5,35 @@ "close_tilt": "Chiudi l'inclinazione di {entity_name}", "open": "Apri {entity_name}", "open_tilt": "Apri l'inclinazione di {entity_name}", - "set_position": "Imposta la posizione di {entity_name}", - "set_tilt_position": "Imposta la posizione di inclinazione di {entity_name}", + "set_position": "Imposta l'apertura di {entity_name}", + "set_tilt_position": "Imposta l'inclinazione di {entity_name}", "stop": "Ferma {entity_name}" }, "condition_type": { - "is_closed": "{entity_name} \u00e8 chiuso", + "is_closed": "{entity_name} \u00e8 chiusa", "is_closing": "{entity_name} si sta chiudendo", - "is_open": "{entity_name} \u00e8 aperto", + "is_open": "{entity_name} \u00e8 aperta", "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" + "is_position": "L'apertura attuale di {entity_name} \u00e8", + "is_tilt_position": "L'inclinazione attuale di {entity_name} \u00e8" }, "trigger_type": { - "closed": "{entity_name} chiuso", + "closed": "{entity_name} chiusa", "closing": "{entity_name} in chiusura", - "opened": "{entity_name} aperto", + "opened": "{entity_name} aperta", "opening": "{entity_name} in apertura", - "position": "{entity_name} cambiamenti della posizione", - "tilt_position": "{entity_name} cambiamenti della posizione d'inclinazione" + "position": "{entity_name} variazioni di apertura", + "tilt_position": "{entity_name} variazioni d'inclinazione" } }, "state": { "_": { - "closed": "Chiuso", + "closed": "Chiusa", "closing": "In chiusura", - "open": "Aperto", + "open": "Aperta", "opening": "In apertura", - "stopped": "Arrestato" + "stopped": "Arrestata" } }, - "title": "Scuri" + "title": "Serrande" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json index 729dc069d7d..cf1a28c3cc2 100644 --- a/homeassistant/components/hassio/translations/cs.json +++ b/homeassistant/components/hassio/translations/cs.json @@ -3,7 +3,7 @@ "info": { "board": "Deska", "disk_total": "Kapacita disku", - "disk_used": "Obsazen\u00fd disk", + "disk_used": "Obsazeno na disku", "docker_version": "Verze Dockeru", "healthy": "V po\u0159\u00e1dku", "host_os": "Hostitelsk\u00fd opera\u010dn\u00ed syst\u00e9m", diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 9e874319246..5faf32e515f 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -6,12 +6,12 @@ "disk_used": "Disco usado", "docker_version": "Versi\u00f3n de Docker", "healthy": "Saludable", - "host_os": "Sistema operativo host", + "host_os": "Sistema operativo del Host", "installed_addons": "Complementos instalados", "supervisor_api": "API del Supervisor", "supervisor_version": "Versi\u00f3n del Supervisor", "supported": "Soportado", - "update_channel": "Actualizar canal", + "update_channel": "Canal de actualizaci\u00f3n", "version_api": "Versi\u00f3n del API" } }, diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json index 23af6c3885e..95b4a8a8a61 100644 --- a/homeassistant/components/hassio/translations/zh-Hans.json +++ b/homeassistant/components/hassio/translations/zh-Hans.json @@ -1,6 +1,17 @@ { "system_health": { "info": { + "board": "\u677f\u5b50", + "disk_total": "\u78c1\u76d8\u5927\u5c0f", + "disk_used": "\u78c1\u76d8\u5df2\u7528", + "docker_version": "Docker \u7248\u672c", + "healthy": "\u5065\u5eb7", + "host_os": "\u5bbf\u4e3b\u64cd\u4f5c\u7cfb\u7edf", + "installed_addons": "\u5df2\u5b89\u88c5\u7684\u52a0\u8f7d\u9879", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor \u7248\u672c", + "supported": "\u53d7\u652f\u6301", + "update_channel": "\u66f4\u65b0\u901a\u9053", "version_api": "API\u7248\u672c" } }, diff --git a/homeassistant/components/homeassistant/translations/zh-Hans.json b/homeassistant/components/homeassistant/translations/zh-Hans.json new file mode 100644 index 00000000000..6d6e1f2eed8 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "CPU \u67b6\u6784", + "chassis": "\u673a\u7bb1", + "dev": "\u5f00\u53d1\u7248", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "\u5b89\u88c5\u7c7b\u578b", + "os_name": "\u64cd\u4f5c\u7cfb\u7edf\u7cfb\u5217", + "os_version": "\u64cd\u4f5c\u7cfb\u7edf\u7248\u672c", + "python_version": "Python \u7248\u672c", + "supervisor": "Supervisor", + "timezone": "\u65f6\u533a", + "version": "\u7248\u672c", + "virtualenv": "\u865a\u62df\u73af\u5883" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index 7020cb163e2..1b7276d4171 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -32,7 +32,7 @@ "data": { "camera_copy": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \uce74\uba54\ub77c" }, - "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 \ub77c\uc988\ubca0\ub9ac\ud30c\uc774\uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi \uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30" }, "init": { diff --git a/homeassistant/components/lovelace/translations/cs.json b/homeassistant/components/lovelace/translations/cs.json index 5c4dc738c6c..f946a859ea2 100644 --- a/homeassistant/components/lovelace/translations/cs.json +++ b/homeassistant/components/lovelace/translations/cs.json @@ -4,7 +4,7 @@ "dashboards": "Dashboardy", "mode": "Re\u017eim", "resources": "Zdroje", - "views": "Zobrazen\u00ed" + "views": "Pohledy" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/zh-Hans.json b/homeassistant/components/lovelace/translations/zh-Hans.json new file mode 100644 index 00000000000..a30b7b2518b --- /dev/null +++ b/homeassistant/components/lovelace/translations/zh-Hans.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "\u4eea\u8868\u76d8", + "mode": "\u6a21\u5f0f", + "resources": "\u8d44\u6e90", + "views": "\u89c6\u56fe" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json index 02271833220..b9a9a1be643 100644 --- a/homeassistant/components/rpi_power/translations/ko.json +++ b/homeassistant/components/rpi_power/translations/ko.json @@ -10,5 +10,5 @@ } } }, - "title": "\ub77c\uc988\ubca0\ub9ac\ud30c\uc774 \uc804\uc6d0 \uacf5\uae09 \uc7a5\uce58 \uac80\uc0ac\uae30" + "title": "Raspberry Pi \uc804\uc6d0 \uacf5\uae09 \uac80\uc0ac" } \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/cs.json b/homeassistant/components/twinkly/translations/cs.json new file mode 100644 index 00000000000..e3b749bfee3 --- /dev/null +++ b/homeassistant/components/twinkly/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "device_exists": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel (nebo IP adresa) va\u0161eho za\u0159\u00edzen\u00ed Twinkly" + }, + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/et.json b/homeassistant/components/twinkly/translations/et.json new file mode 100644 index 00000000000..99e417685ae --- /dev/null +++ b/homeassistant/components/twinkly/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Twinkly seadme host (v\u00f5i IP-aadress)" + }, + "description": "Seadista oma Twinkly LED riba", + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/ru.json b/homeassistant/components/twinkly/translations/ru.json new file mode 100644 index 00000000000..c9ea2c6927b --- /dev/null +++ b/homeassistant/components/twinkly/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 (\u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441) \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Twinkly" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u043e\u0439 \u043b\u0435\u043d\u0442\u044b Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index 66d4b2fb914..e7efb348332 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome, le app e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza." }, "error": { diff --git a/homeassistant/components/vizio/translations/ru.json b/homeassistant/components/vizio/translations/ru.json index 4ae162c0a6e..083c9d93dd3 100644 --- a/homeassistant/components/vizio/translations/ru.json +++ b/homeassistant/components/vizio/translations/ru.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "updated_entry": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { From 3a42277130a0906bf2dbb2b7f052288d6124a2ab Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 19 Nov 2020 19:26:44 -0700 Subject: [PATCH 142/430] Update pymyq to 2.0.10 (#43413) * Bump version of pymyq to 2.0.9 * Bump version of pymyq to 2.0.10 --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 12afcb97d56..0e3d53be081 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.8"], + "requirements": ["pymyq==2.0.10"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index b64e4184785..8f7a77aa07c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1533,7 +1533,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.8 +pymyq==2.0.10 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0691062b53..554ebe0d9af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -770,7 +770,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.8 +pymyq==2.0.10 # homeassistant.components.nut pynut2==2.1.2 From 88367dc46659fe6d23d8e8d0ee7a10ed445b82cf Mon Sep 17 00:00:00 2001 From: Andrew Hayworth Date: Fri, 20 Nov 2020 02:07:59 -0600 Subject: [PATCH 143/430] Update python-awair to 0.2.1 (#43415) This version of python awair has support for the local api - which is cool! But more importantly, this version also is less strict about the versions of aiohttp it depends on. This version prepares for the upcoming pip resolver changes outlined here: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html#what-will-change cc https://github.com/ahayworth/python_awair/issues/18 --- homeassistant/components/awair/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index 8ae89951442..f95e1c19d42 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -2,7 +2,7 @@ "domain": "awair", "name": "Awair", "documentation": "https://www.home-assistant.io/integrations/awair", - "requirements": ["python_awair==0.1.1"], + "requirements": ["python_awair==0.2.1"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 8f7a77aa07c..f98a2418519 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,7 +1830,7 @@ python-whois==0.7.3 python-wink==1.10.5 # homeassistant.components.awair -python_awair==0.1.1 +python_awair==0.2.1 # homeassistant.components.swiss_public_transport python_opendata_transport==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 554ebe0d9af..25d2251879f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -893,7 +893,7 @@ python-twitch-client==0.6.0 python-velbus==2.1.1 # homeassistant.components.awair -python_awair==0.1.1 +python_awair==0.2.1 # homeassistant.components.tile pytile==4.0.0 From 6dc84654b0699942fdd5784563f02a693ee114b9 Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 20 Nov 2020 18:23:38 +1000 Subject: [PATCH 144/430] Increase Advantage Air retry limit for older systems (#43417) --- homeassistant/components/advantage_air/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/advantage_air/const.py b/homeassistant/components/advantage_air/const.py index 078c266bfb0..5c044481ca0 100644 --- a/homeassistant/components/advantage_air/const.py +++ b/homeassistant/components/advantage_air/const.py @@ -1,6 +1,6 @@ """Constants used by Advantage Air integration.""" DOMAIN = "advantage_air" -ADVANTAGE_AIR_RETRY = 5 +ADVANTAGE_AIR_RETRY = 10 ADVANTAGE_AIR_STATE_OPEN = "open" ADVANTAGE_AIR_STATE_CLOSE = "close" ADVANTAGE_AIR_STATE_ON = "on" From 2ed27fc15da350fcd5b8b9e0cd5e4e4593dd66d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Fri, 20 Nov 2020 14:05:51 +0100 Subject: [PATCH 145/430] Update zigpy-zigate to 0.7.3 (#43427) * Update zigpy-zigate to 0.7.3 Fix probing pizigate * Update zigpy-zigate to 0.7.3 Fix probing pizigate --- 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 60757f88098..176338e02c8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.11.0", "zigpy==0.27.1", "zigpy-xbee==0.13.0", - "zigpy-zigate==0.7.2", + "zigpy-zigate==0.7.3", "zigpy-znp==0.2.2" ], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index f98a2418519..6a7687894ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2356,7 +2356,7 @@ zigpy-deconz==0.11.0 zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.7.2 +zigpy-zigate==0.7.3 # homeassistant.components.zha zigpy-znp==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25d2251879f..0fd23400596 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1137,7 +1137,7 @@ zigpy-deconz==0.11.0 zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.7.2 +zigpy-zigate==0.7.3 # homeassistant.components.zha zigpy-znp==0.2.2 From c42b1f65b3447f376cc9eec12cb8dd623e348fbf Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 20 Nov 2020 06:18:02 -0700 Subject: [PATCH 146/430] Add Srp energy component (#41091) --- CODEOWNERS | 1 + .../components/srp_energy/__init__.py | 52 ++++++ .../components/srp_energy/config_flow.py | 71 ++++++++ homeassistant/components/srp_energy/const.py | 15 ++ .../components/srp_energy/manifest.json | 16 ++ homeassistant/components/srp_energy/sensor.py | 153 ++++++++++++++++++ .../components/srp_energy/strings.json | 24 +++ .../srp_energy/translations/en.json | 24 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/srp_energy/__init__.py | 55 +++++++ .../components/srp_energy/test_config_flow.py | 105 ++++++++++++ tests/components/srp_energy/test_init.py | 26 +++ tests/components/srp_energy/test_sensor.py | 129 +++++++++++++++ 15 files changed, 678 insertions(+) create mode 100644 homeassistant/components/srp_energy/__init__.py create mode 100644 homeassistant/components/srp_energy/config_flow.py create mode 100644 homeassistant/components/srp_energy/const.py create mode 100644 homeassistant/components/srp_energy/manifest.json create mode 100644 homeassistant/components/srp_energy/sensor.py create mode 100644 homeassistant/components/srp_energy/strings.json create mode 100644 homeassistant/components/srp_energy/translations/en.json create mode 100644 tests/components/srp_energy/__init__.py create mode 100644 tests/components/srp_energy/test_config_flow.py create mode 100644 tests/components/srp_energy/test_init.py create mode 100644 tests/components/srp_energy/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7465c42a272..0cf31095b52 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -424,6 +424,7 @@ homeassistant/components/splunk/* @Bre77 homeassistant/components/spotify/* @frenck homeassistant/components/sql/* @dgomes homeassistant/components/squeezebox/* @rajlaud +homeassistant/components/srp_energy/* @briglx homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py new file mode 100644 index 00000000000..f7cc1ff8c16 --- /dev/null +++ b/homeassistant/components/srp_energy/__init__.py @@ -0,0 +1,52 @@ +"""The SRP Energy integration.""" +import logging + +from srpenergy.client import SrpEnergyClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import SRP_ENERGY_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +PLATFORMS = ["sensor"] + + +async def async_setup(hass, config): + """Old way of setting up the srp_energy component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up the SRP Energy component from a config entry.""" + # Store an SrpEnergyClient object for your srp_energy to access + try: + srp_energy_client = SrpEnergyClient( + entry.data.get(CONF_ID), + entry.data.get(CONF_USERNAME), + entry.data.get(CONF_PASSWORD), + ) + hass.data[SRP_ENERGY_DOMAIN] = srp_energy_client + except (Exception) as ex: + _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex)) + raise ConfigEntryNotReady from ex + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + # unload srp client + hass.data[SRP_ENERGY_DOMAIN] = None + # Remove config entry + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + + return True diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py new file mode 100644 index 00000000000..b65b93e0108 --- /dev/null +++ b/homeassistant/components/srp_energy/config_flow.py @@ -0,0 +1,71 @@ +"""Config flow for SRP Energy.""" +import logging + +from srpenergy.client import SrpEnergyClient +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME + +from .const import ( # pylint:disable=unused-import + CONF_IS_TOU, + DEFAULT_NAME, + SRP_ENERGY_DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=SRP_ENERGY_DOMAIN): + """Handle a config flow for SRP Energy.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + config = { + vol.Required(CONF_ID): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Optional(CONF_IS_TOU, default=False): bool, + } + + 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="single_instance_allowed") + + if user_input is not None: + try: + + srp_client = SrpEnergyClient( + user_input[CONF_ID], + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + is_valid = await self.hass.async_add_executor_job(srp_client.validate) + + if is_valid: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + errors["base"] = "invalid_auth" + + except ValueError: + errors["base"] = "invalid_account" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(self.config), errors=errors + ) + + async def async_step_import(self, import_config): + """Import from config.""" + # Validate config values + return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/srp_energy/const.py b/homeassistant/components/srp_energy/const.py new file mode 100644 index 00000000000..527a1ed78b1 --- /dev/null +++ b/homeassistant/components/srp_energy/const.py @@ -0,0 +1,15 @@ +"""Constants for the SRP Energy integration.""" +from datetime import timedelta + +SRP_ENERGY_DOMAIN = "srp_energy" +DEFAULT_NAME = "SRP Energy" + +CONF_IS_TOU = "is_tou" + +ATTRIBUTION = "Powered by SRP Energy" +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440) + +SENSOR_NAME = "Usage" +SENSOR_TYPE = "usage" + +ICON = "mdi:flash" diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json new file mode 100644 index 00000000000..fb051fc7b2f --- /dev/null +++ b/homeassistant/components/srp_energy/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "srp_energy", + "name": "SRP Energy", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/srp_energy", + "requirements": [ + "srpenergy==1.3.2" + ], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@briglx" + ] +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py new file mode 100644 index 00000000000..36a8798b05b --- /dev/null +++ b/homeassistant/components/srp_energy/sensor.py @@ -0,0 +1,153 @@ +"""Support for SRP Energy Sensor.""" +from datetime import datetime, timedelta +import logging + +import async_timeout +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout + +from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR +from homeassistant.helpers import entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTRIBUTION, + DEFAULT_NAME, + ICON, + MIN_TIME_BETWEEN_UPDATES, + SENSOR_NAME, + SENSOR_TYPE, + SRP_ENERGY_DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the SRP Energy Usage sensor.""" + # API object stored here by __init__.py + is_time_of_use = False + api = hass.data[SRP_ENERGY_DOMAIN] + if entry and entry.data: + is_time_of_use = entry.data["is_tou"] + + async def async_update_data(): + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + try: + # Fetch srp_energy data + start_date = datetime.now() + timedelta(days=-1) + end_date = datetime.now() + with async_timeout.timeout(10): + hourly_usage = await hass.async_add_executor_job( + api.usage, + start_date, + end_date, + is_time_of_use, + ) + + previous_daily_usage = 0.0 + for _, _, _, kwh, _ in hourly_usage: + previous_daily_usage += float(kwh) + return previous_daily_usage + except (TimeoutError) as timeout_err: + raise UpdateFailed("Timeout communicating with API") from timeout_err + except (ConnectError, HTTPError, Timeout, ValueError, TypeError) as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sensor", + update_method=async_update_data, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + async_add_entities([SrpEntity(coordinator)]) + + +class SrpEntity(entity.Entity): + """Implementation of a Srp Energy Usage sensor.""" + + def __init__(self, coordinator): + """Initialize the SrpEntity class.""" + self._name = SENSOR_NAME + self.type = SENSOR_TYPE + self.coordinator = coordinator + self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return f"{DEFAULT_NAME} {self._name}" + + @property + def unique_id(self): + """Return sensor unique_id.""" + return self.type + + @property + def state(self): + """Return the state of the device.""" + if self._state: + return f"{self._state:.2f}" + return None + + @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): + """Return icon.""" + return ICON + + @property + def usage(self): + """Return entity state.""" + if self.coordinator.data: + return f"{self.coordinator.data:.2f}" + return None + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if not self.coordinator.data: + return None + attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + return attributes + + @property + def available(self): + """Return if entity is available.""" + return self.coordinator.last_update_success + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + if self.coordinator.data: + self._state = self.coordinator.data + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json new file mode 100644 index 00000000000..8dce61229a9 --- /dev/null +++ b/homeassistant/components/srp_energy/strings.json @@ -0,0 +1,24 @@ +{ + "title": "SRP Energy", + "config": { + "step": { + "user": { + "data": { + "id": "Account Id", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "is_tou": "Is Time of Use Plan" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_account": "Account ID should be a 9 digit number", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/srp_energy/translations/en.json b/homeassistant/components/srp_energy/translations/en.json new file mode 100644 index 00000000000..0d00872e43d --- /dev/null +++ b/homeassistant/components/srp_energy/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_account": "Account ID should be a 9 digit number", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "id": "Account Id", + "username": "Username", + "password": "Password", + "is_tou": "Is Time of Use Plan" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5386ff4ce60..531e575dc0a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -190,6 +190,7 @@ FLOWS = [ "spider", "spotify", "squeezebox", + "srp_energy", "starline", "syncthru", "synology_dsm", diff --git a/requirements_all.txt b/requirements_all.txt index 6a7687894ec..f4905beb679 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2098,6 +2098,9 @@ spotipy==2.16.1 # homeassistant.components.sql sqlalchemy==1.3.20 +# homeassistant.components.srp_energy +srpenergy==1.3.2 + # homeassistant.components.starline starline==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fd23400596..7160913d7af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,6 +1017,9 @@ spotipy==2.16.1 # homeassistant.components.sql sqlalchemy==1.3.20 +# homeassistant.components.srp_energy +srpenergy==1.3.2 + # homeassistant.components.starline starline==0.1.3 diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py new file mode 100644 index 00000000000..34f06e2993e --- /dev/null +++ b/tests/components/srp_energy/__init__.py @@ -0,0 +1,55 @@ +"""Tests for the SRP Energy integration.""" +from homeassistant import config_entries +from homeassistant.components import srp_energy +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +ENTRY_OPTIONS = {} + +ENTRY_CONFIG = { + CONF_NAME: "Test", + CONF_ID: "123456789", + CONF_USERNAME: "abba", + CONF_PASSWORD: "ana", + srp_energy.const.CONF_IS_TOU: False, +} + + +async def init_integration( + hass, + config=None, + options=None, + entry_id="1", + source="user", + side_effect=None, + usage=None, +): + """Set up the srp_energy integration in Home Assistant.""" + if not config: + config = ENTRY_CONFIG + + if not options: + options = ENTRY_OPTIONS + + config_entry = MockConfigEntry( + domain=srp_energy.SRP_ENERGY_DOMAIN, + source=source, + data=config, + connection_class=config_entries.CONN_CLASS_CLOUD_POLL, + options=options, + entry_id=entry_id, + ) + + with patch("srpenergy.client.SrpEnergyClient"), patch( + "homeassistant.components.srp_energy.SrpEnergyClient", side_effect=side_effect + ), patch("srpenergy.client.SrpEnergyClient.usage", return_value=usage), patch( + "homeassistant.components.srp_energy.SrpEnergyClient.usage", return_value=usage + ): + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py new file mode 100644 index 00000000000..acb9d28f75d --- /dev/null +++ b/tests/components/srp_energy/test_config_flow.py @@ -0,0 +1,105 @@ +"""Test the SRP Energy config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.srp_energy.const import CONF_IS_TOU, SRP_ENERGY_DOMAIN + +from . import ENTRY_CONFIG, init_integration + +from tests.async_mock import patch + + +async def test_form(hass): + """Test user config.""" + # First get the form + result = await hass.config_entries.flow.async_init( + SRP_ENERGY_DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + # Fill submit form data for config entry + with patch("homeassistant.components.srp_energy.config_flow.SrpEnergyClient"): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Test" + assert result["data"][CONF_IS_TOU] is False + + +async def test_form_invalid_auth(hass): + """Test user config with invalid auth.""" + result = await hass.config_entries.flow.async_init( + SRP_ENERGY_DOMAIN, context={"source": "user"} + ) + + with patch( + "homeassistant.components.srp_energy.config_flow.SrpEnergyClient.validate", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) + + assert result["errors"]["base"] == "invalid_auth" + + +async def test_form_value_error(hass): + """Test user config that throws a value error.""" + result = await hass.config_entries.flow.async_init( + SRP_ENERGY_DOMAIN, context={"source": "user"} + ) + + with patch( + "homeassistant.components.srp_energy.config_flow.SrpEnergyClient", + side_effect=ValueError(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) + + assert result["errors"]["base"] == "invalid_account" + + +async def test_form_unknown_exception(hass): + """Test user config that throws an unknown exception.""" + result = await hass.config_entries.flow.async_init( + SRP_ENERGY_DOMAIN, context={"source": "user"} + ) + + with patch( + "homeassistant.components.srp_energy.config_flow.SrpEnergyClient", + side_effect=Exception(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) + + assert result["errors"]["base"] == "unknown" + + +async def test_config(hass): + """Test handling of configuration imported.""" + with patch("homeassistant.components.srp_energy.config_flow.SrpEnergyClient"): + result = await hass.config_entries.flow.async_init( + SRP_ENERGY_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_integration_already_configured(hass): + """Test integration is already configured.""" + await init_integration(hass) + result = await hass.config_entries.flow.async_init( + SRP_ENERGY_DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py new file mode 100644 index 00000000000..8e758d05114 --- /dev/null +++ b/tests/components/srp_energy/test_init.py @@ -0,0 +1,26 @@ +"""Tests for Srp Energy component Init.""" +from homeassistant.components import srp_energy + +from tests.components.srp_energy import init_integration + + +async def test_setup_entry(hass): + """Test setup entry fails if deCONZ is not available.""" + config_entry = await init_integration(hass) + assert config_entry.state == "loaded" + assert hass.data[srp_energy.SRP_ENERGY_DOMAIN] + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + config_entry = await init_integration(hass) + assert hass.data[srp_energy.SRP_ENERGY_DOMAIN] + + assert await srp_energy.async_unload_entry(hass, config_entry) + assert not hass.data[srp_energy.SRP_ENERGY_DOMAIN] + + +async def test_async_setup_entry_with_exception(hass): + """Test exception when SrpClient can't load.""" + await init_integration(hass, side_effect=Exception()) + assert srp_energy.SRP_ENERGY_DOMAIN not in hass.data diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py new file mode 100644 index 00000000000..3a70a3ec09f --- /dev/null +++ b/tests/components/srp_energy/test_sensor.py @@ -0,0 +1,129 @@ +"""Tests for the srp_energy sensor platform.""" +from homeassistant.components.srp_energy.const import ( + ATTRIBUTION, + DEFAULT_NAME, + ICON, + SENSOR_NAME, + SENSOR_TYPE, + SRP_ENERGY_DOMAIN, +) +from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry +from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR + +from tests.async_mock import MagicMock + + +async def test_async_setup_entry(hass): + """Test the sensor.""" + fake_async_add_entities = MagicMock() + fake_srp_energy_client = MagicMock() + fake_srp_energy_client.usage.return_value = [{1, 2, 3, 1.999, 4}] + fake_config = MagicMock( + data={ + "name": "SRP Energy", + "is_tou": False, + "id": "0123456789", + "username": "testuser@example.com", + "password": "mypassword", + } + ) + hass.data[SRP_ENERGY_DOMAIN] = fake_srp_energy_client + + await async_setup_entry(hass, fake_config, fake_async_add_entities) + + +async def test_async_setup_entry_timeout_error(hass): + """Test fetching usage data. Failed the first time because was too get response.""" + fake_async_add_entities = MagicMock() + fake_srp_energy_client = MagicMock() + fake_srp_energy_client.usage.return_value = [{1, 2, 3, 1.999, 4}] + fake_config = MagicMock( + data={ + "name": "SRP Energy", + "is_tou": False, + "id": "0123456789", + "username": "testuser@example.com", + "password": "mypassword", + } + ) + hass.data[SRP_ENERGY_DOMAIN] = fake_srp_energy_client + fake_srp_energy_client.usage.side_effect = TimeoutError() + + await async_setup_entry(hass, fake_config, fake_async_add_entities) + assert not fake_async_add_entities.call_args[0][0][ + 0 + ].coordinator.last_update_success + + +async def test_async_setup_entry_connect_error(hass): + """Test fetching usage data. Failed the first time because was too get response.""" + fake_async_add_entities = MagicMock() + fake_srp_energy_client = MagicMock() + fake_srp_energy_client.usage.return_value = [{1, 2, 3, 1.999, 4}] + fake_config = MagicMock( + data={ + "name": "SRP Energy", + "is_tou": False, + "id": "0123456789", + "username": "testuser@example.com", + "password": "mypassword", + } + ) + hass.data[SRP_ENERGY_DOMAIN] = fake_srp_energy_client + fake_srp_energy_client.usage.side_effect = ValueError() + + await async_setup_entry(hass, fake_config, fake_async_add_entities) + assert not fake_async_add_entities.call_args[0][0][ + 0 + ].coordinator.last_update_success + + +async def test_srp_entity(hass): + """Test the SrpEntity.""" + fake_coordinator = MagicMock(data=1.99999999999) + srp_entity = SrpEntity(fake_coordinator) + + assert srp_entity is not None + assert srp_entity.name == f"{DEFAULT_NAME} {SENSOR_NAME}" + assert srp_entity.unique_id == SENSOR_TYPE + assert srp_entity.state is None + assert srp_entity.unit_of_measurement == ENERGY_KILO_WATT_HOUR + assert srp_entity.icon == ICON + assert srp_entity.usage == "2.00" + assert srp_entity.should_poll is False + assert srp_entity.device_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + assert srp_entity.available is not None + + await srp_entity.async_added_to_hass() + assert srp_entity.state is not None + assert fake_coordinator.async_add_listener.called + assert not fake_coordinator.async_add_listener.data.called + + +async def test_srp_entity_no_data(hass): + """Test the SrpEntity.""" + fake_coordinator = MagicMock(data=False) + srp_entity = SrpEntity(fake_coordinator) + assert srp_entity.device_state_attributes is None + + +async def test_srp_entity_no_coord_data(hass): + """Test the SrpEntity.""" + fake_coordinator = MagicMock(data=False) + srp_entity = SrpEntity(fake_coordinator) + + assert srp_entity.usage is None + + +async def test_srp_entity_async_update(hass): + """Test the SrpEntity.""" + + async def async_magic(): + pass + + MagicMock.__await__ = lambda x: async_magic().__await__() + fake_coordinator = MagicMock(data=False) + srp_entity = SrpEntity(fake_coordinator) + + await srp_entity.async_update() + assert fake_coordinator.async_request_refresh.called From 9b3c97345a5bd25e570d01f1000ee4a9fe9b697e Mon Sep 17 00:00:00 2001 From: 100ferhas <46828393+100ferhas@users.noreply.github.com> Date: Fri, 20 Nov 2020 14:47:17 +0100 Subject: [PATCH 147/430] Update Alexa supported languages (#43139) --- .../components/alexa/capabilities.py | 93 +++++++++++++++++-- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 1163d0de101..ac4784132f5 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -412,10 +412,17 @@ class AlexaLockController(AlexaCapability): "en-AU", "en-CA", "en-GB", + "en-IN", "en-US", "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -454,6 +461,7 @@ class AlexaSceneController(AlexaCapability): supported_locales = { "de-DE", + "en-AU", "en-CA", "en-GB", "en-IN", @@ -461,6 +469,7 @@ class AlexaSceneController(AlexaCapability): "es-ES", "fr-FR", "it-IT", + "ja-JP", } def __init__(self, entity, supports_deactivation): @@ -488,8 +497,10 @@ class AlexaBrightnessController(AlexaCapability): "en-US", "es-ES", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -532,8 +543,10 @@ class AlexaColorController(AlexaCapability): "en-US", "es-ES", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -581,8 +594,10 @@ class AlexaColorTemperatureController(AlexaCapability): "en-US", "es-ES", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): @@ -669,7 +684,18 @@ class AlexaSpeaker(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-speaker.html """ - supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "it-IT", + "ja-JP", + } def name(self): """Return the Alexa API name of this interface.""" @@ -716,7 +742,16 @@ class AlexaStepSpeaker(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html """ - supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "it-IT", + } def name(self): """Return the Alexa API name of this interface.""" @@ -866,7 +901,16 @@ class AlexaContactSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html """ - supported_locales = {"en-CA", "en-US", "it-IT"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-IN", + "en-US", + "es-ES", + "it-IT", + "ja-JP", + } def __init__(self, hass, entity): """Initialize the entity.""" @@ -905,7 +949,17 @@ class AlexaMotionSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html """ - supported_locales = {"en-CA", "en-US", "it-IT"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-IN", + "en-US", + "es-ES", + "it-IT", + "ja-JP", + "pt-BR", + } def __init__(self, hass, entity): """Initialize the entity.""" @@ -955,6 +1009,7 @@ class AlexaThermostatController(AlexaCapability): "fr-FR", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, hass, entity): @@ -1127,7 +1182,7 @@ class AlexaSecurityPanelController(AlexaCapability): "fr-FR", "it-IT", "ja-JP", - "pt_BR", + "pt-BR", } def __init__(self, hass, entity): @@ -1623,6 +1678,7 @@ class AlexaToggleController(AlexaCapability): "fr-FR", "it-IT", "ja-JP", + "pt-BR", } def __init__(self, entity, instance, non_controllable=False): @@ -1679,7 +1735,21 @@ class AlexaChannelController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html """ - supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } def name(self): """Return the Alexa API name of this interface.""" @@ -1693,7 +1763,6 @@ class AlexaDoorbellEventSource(AlexaCapability): """ supported_locales = { - "en-US", "de-DE", "en-AU", "en-CA", @@ -1702,8 +1771,10 @@ class AlexaDoorbellEventSource(AlexaCapability): "en-US", "es-ES", "es-MX", + "es-US", "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", } @@ -1723,7 +1794,7 @@ class AlexaPlaybackStateReporter(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html """ - supported_locales = {"de-DE", "en-GB", "en-US", "fr-FR"} + supported_locales = {"de-DE", "en-GB", "en-US", "es-MX", "fr-FR"} def name(self): """Return the Alexa API name of this interface.""" @@ -1761,7 +1832,7 @@ class AlexaSeekController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html """ - supported_locales = {"de-DE", "en-GB", "en-US"} + supported_locales = {"de-DE", "en-GB", "en-US", "es-MX"} def name(self): """Return the Alexa API name of this interface.""" @@ -1833,7 +1904,7 @@ class AlexaEqualizerController(AlexaCapability): https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html """ - supported_locales = {"en-US"} + supported_locales = {"de-DE", "en-IN", "en-US", "es-ES", "it-IT", "ja-JP", "pt-BR"} VALID_SOUND_MODES = { "MOVIE", "MUSIC", @@ -1929,8 +2000,10 @@ class AlexaCameraStreamController(AlexaCapability): "en-US", "es-ES", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", } def name(self): From e98f36e357c65af1bd2a14da61e732db0784750f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 20 Nov 2020 15:20:09 +0100 Subject: [PATCH 148/430] Support openRelativePercent for google assistant covers (#43336) --- .../components/google_assistant/trait.py | 45 ++++++++--- .../google_assistant/test_smart_home.py | 2 +- .../components/google_assistant/test_trait.py | 81 ++++++++++++++++--- 3 files changed, 106 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 55c26b9499d..b8d21c0c77b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -68,7 +68,6 @@ from .const import ( ERR_ALREADY_ARMED, ERR_ALREADY_DISARMED, ERR_CHALLENGE_NOT_SETUP, - ERR_FUNCTION_NOT_SUPPORTED, ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, ERR_VALUE_OUT_OF_RANGE, @@ -120,6 +119,7 @@ COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput" COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput" COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput" COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose" +COMMAND_OPENCLOSE_RELATIVE = f"{PREFIX_COMMANDS}OpenCloseRelative" COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume" COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative" COMMAND_MUTE = f"{PREFIX_COMMANDS}mute" @@ -1519,7 +1519,7 @@ class OpenCloseTrait(_Trait): ) name = TRAIT_OPENCLOSE - commands = [COMMAND_OPENCLOSE] + commands = [COMMAND_OPENCLOSE, COMMAND_OPENCLOSE_RELATIVE] @staticmethod def supported(domain, features, device_class): @@ -1543,9 +1543,20 @@ class OpenCloseTrait(_Trait): def sync_attributes(self): """Return opening direction.""" response = {} + features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if self.state.domain == binary_sensor.DOMAIN: response["queryOnlyOpenClose"] = True response["discreteOnlyOpenClose"] = True + elif self.state.domain == cover.DOMAIN: + if features & cover.SUPPORT_SET_POSITION == 0: + response["discreteOnlyOpenClose"] = True + + if ( + features & cover.SUPPORT_OPEN == 0 + and features & cover.SUPPORT_CLOSE == 0 + ): + response["queryOnlyOpenClose"] = True if self.state.attributes.get(ATTR_ASSUMED_STATE): response["commandOnlyOpenClose"] = True @@ -1590,26 +1601,36 @@ class OpenCloseTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an Open, close, Set position command.""" domain = self.state.domain + features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if domain == cover.DOMAIN: svc_params = {ATTR_ENTITY_ID: self.state.entity_id} + should_verify = False + if command == COMMAND_OPENCLOSE_RELATIVE: + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) + if position is None: + raise SmartHomeError( + ERR_NOT_SUPPORTED, + "Current position not know for relative command", + ) + position = max(0, min(100, position + params["openRelativePercent"])) + else: + position = params["openPercent"] - if params["openPercent"] == 0: + if features & cover.SUPPORT_SET_POSITION: + service = cover.SERVICE_SET_COVER_POSITION + if position > 0: + should_verify = True + svc_params[cover.ATTR_POSITION] = position + elif position == 0: service = cover.SERVICE_CLOSE_COVER should_verify = False - elif params["openPercent"] == 100: + elif position == 100: service = cover.SERVICE_OPEN_COVER should_verify = True - elif ( - self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & cover.SUPPORT_SET_POSITION - ): - service = cover.SERVICE_SET_COVER_POSITION - should_verify = True - svc_params[cover.ATTR_POSITION] = params["openPercent"] else: raise SmartHomeError( - ERR_FUNCTION_NOT_SUPPORTED, "Setting a position is not supported" + ERR_NOT_SUPPORTED, "No support for partial open close" ) if ( diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index f35415ee9e4..3fcab2dc2a2 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -839,7 +839,7 @@ async def test_device_class_cover(hass, device_class, google_type): "agentUserId": "test-agent", "devices": [ { - "attributes": {}, + "attributes": {"discreteOnlyOpenClose": True}, "id": "cover.demo_sensor", "name": {"name": "Demo Sensor"}, "traits": ["action.devices.traits.OpenClose"], diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a579eebea04..2ca2e6c8e6c 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1860,8 +1860,12 @@ async def test_openclose_cover(hass): calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {}) - assert len(calls) == 1 + await trt.execute( + trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {} + ) + assert len(calls) == 2 assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50} + assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 100} async def test_openclose_cover_unknown_state(hass): @@ -1873,10 +1877,14 @@ async def test_openclose_cover_unknown_state(hass): # No state trt = trait.OpenCloseTrait( - hass, State("cover.bla", STATE_UNKNOWN, {}), BASIC_CONFIG + hass, + State( + "cover.bla", STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN} + ), + BASIC_CONFIG, ) - assert trt.sync_attributes() == {} + assert trt.sync_attributes() == {"discreteOnlyOpenClose": True} with pytest.raises(helpers.SmartHomeError): trt.query_attributes() @@ -1920,25 +1928,81 @@ async def test_openclose_cover_assumed_state(hass): assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 40} +async def test_openclose_cover_query_only(hass): + """Test OpenClose trait support for cover domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None + assert trait.OpenCloseTrait.supported(cover.DOMAIN, 0, None) + + state = State( + "cover.bla", + cover.STATE_OPEN, + ) + + trt = trait.OpenCloseTrait( + hass, + state, + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "discreteOnlyOpenClose": True, + "queryOnlyOpenClose": True, + } + assert trt.query_attributes() == {"openPercent": 100} + + async def test_openclose_cover_no_position(hass): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None + cover.DOMAIN, cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, None + ) + + state = State( + "cover.bla", + cover.STATE_OPEN, + { + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, + }, ) trt = trait.OpenCloseTrait( - hass, State("cover.bla", cover.STATE_OPEN, {}), BASIC_CONFIG + hass, + state, + BASIC_CONFIG, ) - assert trt.sync_attributes() == {} + assert trt.sync_attributes() == {"discreteOnlyOpenClose": True} assert trt.query_attributes() == {"openPercent": 100} + state.state = cover.STATE_CLOSED + + assert trt.sync_attributes() == {"discreteOnlyOpenClose": True} + assert trt.query_attributes() == {"openPercent": 0} + calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 0}, {}) assert len(calls) == 1 assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} + calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) + await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 100}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} + + with pytest.raises( + SmartHomeError, match=r"Current position not know for relative command" + ): + await trt.execute( + trait.COMMAND_OPENCLOSE_RELATIVE, + BASIC_DATA, + {"openRelativePercent": 100}, + {}, + ) + + with pytest.raises(SmartHomeError, match=r"No support for partial open close"): + await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {}) + @pytest.mark.parametrize( "device_class", @@ -1996,10 +2060,9 @@ async def test_openclose_cover_secure(hass, device_class): assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50} # no challenge on close - calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER) await trt.execute(trait.COMMAND_OPENCLOSE, PIN_DATA, {"openPercent": 0}, {}) - assert len(calls) == 1 - assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} + assert len(calls) == 2 + assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 0} @pytest.mark.parametrize( From 43ba053030f6dd817a663f4bc85482f6347188c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Nov 2020 15:24:42 +0100 Subject: [PATCH 149/430] Add support for checking minimum HA version (#43350) --- homeassistant/components/blueprint/const.py | 2 + homeassistant/components/blueprint/models.py | 26 +++++++++-- homeassistant/components/blueprint/schemas.py | 27 +++++++++++- .../components/blueprint/websocket_api.py | 1 + homeassistant/helpers/selector.py | 5 +++ tests/components/blueprint/test_models.py | 30 ++++++++++++- tests/components/blueprint/test_schemas.py | 43 +++++++++++++++++++ .../blueprint/test_websocket_api.py | 1 + 8 files changed, 130 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blueprint/const.py b/homeassistant/components/blueprint/const.py index d9e3839f026..60df20dda36 100644 --- a/homeassistant/components/blueprint/const.py +++ b/homeassistant/components/blueprint/const.py @@ -6,5 +6,7 @@ CONF_USE_BLUEPRINT = "use_blueprint" CONF_INPUT = "input" CONF_SOURCE_URL = "source_url" CONF_DESCRIPTION = "description" +CONF_HOMEASSISTANT = "homeassistant" +CONF_MIN_VERSION = "min_version" DOMAIN = "blueprint" diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 1681b4ffd31..48390d0bd23 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -2,12 +2,13 @@ import asyncio import logging import pathlib -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union +from pkg_resources import parse_version import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH +from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import placeholder @@ -16,7 +17,9 @@ from homeassistant.util import yaml from .const import ( BLUEPRINT_FOLDER, CONF_BLUEPRINT, + CONF_HOMEASSISTANT, CONF_INPUT, + CONF_MIN_VERSION, CONF_SOURCE_URL, CONF_USE_BLUEPRINT, DOMAIN, @@ -62,7 +65,7 @@ class Blueprint: self.domain = data_domain - missing = self.placeholders - set(data[CONF_BLUEPRINT].get(CONF_INPUT, {})) + missing = self.placeholders - set(data[CONF_BLUEPRINT][CONF_INPUT]) if missing: raise InvalidBlueprint( @@ -91,6 +94,23 @@ class Blueprint: """Dump blueprint as YAML.""" return yaml.dump(self.data) + @callback + def validate(self) -> Optional[List[str]]: + """Test if the Home Assistant installation supports this blueprint. + + Return list of errors if not valid. + """ + errors = [] + metadata = self.metadata + min_version = metadata.get(CONF_HOMEASSISTANT, {}).get(CONF_MIN_VERSION) + + if min_version is not None and parse_version(__version__) < parse_version( + min_version + ): + errors.append(f"Requires at least Home Assistant {min_version}") + + return errors or None + class BlueprintInputs: """Inputs for a blueprint.""" diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index b734476b85a..ed78b4d2b42 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -10,12 +10,34 @@ from homeassistant.helpers import config_validation as cv, selector from .const import ( CONF_BLUEPRINT, CONF_DESCRIPTION, + CONF_HOMEASSISTANT, CONF_INPUT, + CONF_MIN_VERSION, CONF_SOURCE_URL, CONF_USE_BLUEPRINT, ) +def version_validator(value): + """Validate a Home Assistant version.""" + if not isinstance(value, str): + raise vol.Invalid("Version needs to be a string") + + parts = value.split(".") + + if len(parts) != 3: + raise vol.Invalid("Version needs to be formatted as {major}.{minor}.{patch}") + + try: + parts = [int(p) for p in parts] + except ValueError: + raise vol.Invalid( + "Major, minor and patch version needs to be an integer" + ) from None + + return value + + @callback def is_blueprint_config(config: Any) -> bool: """Return if it is a blueprint config.""" @@ -43,6 +65,9 @@ BLUEPRINT_SCHEMA = vol.Schema( vol.Required(CONF_NAME): str, vol.Required(CONF_DOMAIN): str, vol.Optional(CONF_SOURCE_URL): cv.url, + vol.Optional(CONF_HOMEASSISTANT): { + vol.Optional(CONF_MIN_VERSION): version_validator + }, vol.Optional(CONF_INPUT, default=dict): { str: vol.Any( None, @@ -68,7 +93,7 @@ BLUEPRINT_INSTANCE_FIELDS = vol.Schema( vol.Required(CONF_USE_BLUEPRINT): vol.Schema( { vol.Required(CONF_PATH): vol.All(cv.path, validate_yaml_suffix), - vol.Required(CONF_INPUT): {str: cv.match_all}, + vol.Required(CONF_INPUT, default=dict): {str: cv.match_all}, } ) }, diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 88aa00788be..1e6971d9bc8 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -85,6 +85,7 @@ async def ws_import_blueprint(hass, connection, msg): "blueprint": { "metadata": imported_blueprint.blueprint.metadata, }, + "validation_errors": imported_blueprint.blueprint.validate(), }, ) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index b00ae972c8e..5bf70f01bfa 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -40,7 +40,9 @@ class EntitySelector(Selector): CONFIG_SCHEMA = vol.Schema( { + # Integration that provided the entity vol.Optional("integration"): str, + # Domain the entity belongs to vol.Optional("domain"): str, } ) @@ -52,8 +54,11 @@ class DeviceSelector(Selector): CONFIG_SCHEMA = vol.Schema( { + # Integration linked to it with a config entry vol.Optional("integration"): str, + # Manufacturer of device vol.Optional("manufacturer"): str, + # Model of device vol.Optional("model"): str, } ) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index c66ebcfceb6..48fbf617fa1 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -71,7 +71,7 @@ def test_blueprint_properties(blueprint_1): def test_blueprint_update_metadata(): - """Test properties.""" + """Test update metadata.""" bp = models.Blueprint( { "blueprint": { @@ -85,6 +85,34 @@ def test_blueprint_update_metadata(): assert bp.metadata["source_url"] == "http://bla.com" +def test_blueprint_validate(): + """Test validate blueprint.""" + assert ( + models.Blueprint( + { + "blueprint": { + "name": "Hello", + "domain": "automation", + }, + } + ).validate() + is None + ) + + assert ( + models.Blueprint( + { + "blueprint": { + "name": "Hello", + "domain": "automation", + "homeassistant": {"min_version": "100000.0.0"}, + }, + } + ).validate() + == ["Requires at least Home Assistant 100000.0.0"] + ) + + def test_blueprint_inputs(blueprint_1): """Test blueprint inputs.""" inputs = models.BlueprintInputs( diff --git a/tests/components/blueprint/test_schemas.py b/tests/components/blueprint/test_schemas.py index bf50bdad975..7c91c1e4117 100644 --- a/tests/components/blueprint/test_schemas.py +++ b/tests/components/blueprint/test_schemas.py @@ -31,6 +31,26 @@ _LOGGER = logging.getLogger(__name__) }, } }, + # With selector + { + "blueprint": { + "name": "Test Name", + "domain": "automation", + "input": { + "some_placeholder": {"selector": {"entity": {}}}, + }, + } + }, + # With min version + { + "blueprint": { + "name": "Test Name", + "domain": "automation", + "homeassistant": { + "min_version": "1000000.0.0", + }, + } + }, ), ) def test_blueprint_schema(blueprint): @@ -63,9 +83,32 @@ def test_blueprint_schema(blueprint): "input": {"some_placeholder": {"non_existing": "bla"}}, } }, + # Invalid version + { + "blueprint": { + "name": "Test Name", + "domain": "automation", + "homeassistant": { + "min_version": "1000000.invalid.0", + }, + } + }, ), ) def test_blueprint_schema_invalid(blueprint): """Test different schemas.""" with pytest.raises(vol.Invalid): schemas.BLUEPRINT_SCHEMA(blueprint) + + +@pytest.mark.parametrize( + "bp_instance", + ( + {"path": "hello.yaml"}, + {"path": "hello.yaml", "input": {}}, + {"path": "hello.yaml", "input": {"hello": None}}, + ), +) +def test_blueprint_instance_fields(bp_instance): + """Test blueprint instance fields.""" + schemas.BLUEPRINT_INSTANCE_FIELDS({"use_blueprint": bp_instance}) diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 2459b014c7b..c948494cca0 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -96,6 +96,7 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): "name": "Call service based on event", }, }, + "validation_errors": None, } From 82b7cc8ac7c14cff6646d3c34d9a434866ea70d5 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 20 Nov 2020 15:42:19 +0100 Subject: [PATCH 150/430] Provide HA username via auth WS (#43283) --- homeassistant/components/config/auth.py | 11 +++++++++++ tests/components/config/test_auth.py | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index d5bbb60e27d..440ac05cef4 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -123,8 +123,19 @@ async def websocket_update(hass, connection, msg): def _user_info(user): """Format a user.""" + + ha_username = next( + ( + cred.data.get("username") + for cred in user.credentials + if cred.auth_provider_type == "homeassistant" + ), + None, + ) + return { "id": user.id, + "username": ha_username, "name": user.name, "is_owner": user.is_owner, "is_active": user.is_active, diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index 53defb4cd6e..e7e3c67e5d8 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -34,7 +34,9 @@ async def test_list(hass, hass_ws_client, hass_admin_user): owner.credentials.append( auth_models.Credentials( - auth_provider_type="homeassistant", auth_provider_id=None, data={} + auth_provider_type="homeassistant", + auth_provider_id=None, + data={"username": "test-owner"}, ) ) @@ -58,6 +60,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): assert len(data) == 4 assert data[0] == { "id": hass_admin_user.id, + "username": None, "name": "Mock User", "is_owner": False, "is_active": True, @@ -67,6 +70,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): } assert data[1] == { "id": owner.id, + "username": "test-owner", "name": "Test Owner", "is_owner": True, "is_active": True, @@ -76,6 +80,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): } assert data[2] == { "id": system.id, + "username": None, "name": "Test Hass.io", "is_owner": False, "is_active": True, @@ -85,6 +90,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user): } assert data[3] == { "id": inactive.id, + "username": None, "name": "Inactive User", "is_owner": False, "is_active": False, From bbb82ded689c30ad5948ca4e65e93b244e9b0507 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Nov 2020 15:43:28 +0100 Subject: [PATCH 151/430] Fix time trigger based on entities ignoring entities if initially in the past (#43431) --- .../components/homeassistant/triggers/time.py | 7 +- .../homeassistant/triggers/test_time.py | 92 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index b636a7a3590..b0fced4d55d 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -143,9 +143,12 @@ async def async_attach_trigger(hass, config, action, automation_info): if remove: entities[entity_id] = remove + to_track = [] + for at_time in config[CONF_AT]: if isinstance(at_time, str): # entity + to_track.append(at_time) update_entity_trigger(at_time, new_state=hass.states.get(at_time)) else: # datetime.time @@ -161,9 +164,7 @@ async def async_attach_trigger(hass, config, action, automation_info): # Track state changes of any entities. removes.append( - async_track_state_change_event( - hass, list(entities), update_entity_trigger_event - ) + async_track_state_change_event(hass, to_track, update_entity_trigger_event) ) @callback diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 673d0231912..a37be71102d 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -2,8 +2,10 @@ from datetime import timedelta import pytest +import voluptuous as vol from homeassistant.components import automation, sensor +from homeassistant.components.homeassistant.triggers import time from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -492,3 +494,93 @@ async def test_if_fires_using_at_sensor(hass, calls): # We should not have listened to anything assert len(calls) == 2 + + +@pytest.mark.parametrize( + "conf", + [ + {"platform": "time", "at": "input_datetime.bla"}, + {"platform": "time", "at": "sensor.bla"}, + {"platform": "time", "at": "12:34"}, + ], +) +def test_schema_valid(conf): + """Make sure we don't accept number for 'at' value.""" + time.TRIGGER_SCHEMA(conf) + + +@pytest.mark.parametrize( + "conf", + [ + {"platform": "time", "at": "binary_sensor.bla"}, + {"platform": "time", "at": 745}, + {"platform": "time", "at": "25:00"}, + ], +) +def test_schema_invalid(conf): + """Make sure we don't accept number for 'at' value.""" + with pytest.raises(vol.Invalid): + time.TRIGGER_SCHEMA(conf) + + +async def test_datetime_in_past_on_load(hass, calls): + """Test time trigger works if input_datetime is in past.""" + await async_setup_component( + hass, + "input_datetime", + {"input_datetime": {"my_trigger": {"has_date": True, "has_time": True}}}, + ) + + now = dt_util.now() + past = now - timedelta(days=2) + future = now + timedelta(days=1) + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.my_trigger", + "datetime": str(past.replace(tzinfo=None)), + }, + blocking=True, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "time", "at": "input_datetime.my_trigger"}, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{trigger.entity_id}}" + }, + }, + } + }, + ) + + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + assert len(calls) == 0 + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.my_trigger", + "datetime": str(future.replace(tzinfo=None)), + }, + blocking=True, + ) + + async_fire_time_changed(hass, future + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert ( + calls[0].data["some"] + == f"time-{future.day}-{future.hour}-input_datetime.my_trigger" + ) From dc5f7aedd716347a831844d192b5f219188c8799 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Nov 2020 15:57:57 +0100 Subject: [PATCH 152/430] Verify that we register blueprints on automation setup (#43434) --- homeassistant/components/automation/__init__.py | 3 +++ tests/components/blueprint/test_websocket_api.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 74a319242ec..e91aa687b21 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -163,6 +163,9 @@ async def async_setup(hass, config): """Set up the automation.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + # To register the automation blueprints + async_get_blueprints(hass) + await _async_process_config(hass, config, component) async def trigger_service_handler(entity, service_call): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index c948494cca0..32b7e7748e5 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -3,7 +3,6 @@ from pathlib import Path import pytest -from homeassistant.components import automation from homeassistant.setup import async_setup_component from tests.async_mock import Mock, patch @@ -15,7 +14,7 @@ async def setup_bp(hass): assert await async_setup_component(hass, "blueprint", {}) # Trigger registration of automation blueprints - automation.async_get_blueprints(hass) + await async_setup_component(hass, "automation", {}) async def test_list_blueprints(hass, hass_ws_client): From 7708da67136ef1cd082f721c39e48f9f58d14438 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Fri, 20 Nov 2020 16:47:47 +0100 Subject: [PATCH 153/430] Update dsmr_parser to 0.23 (#43403) --- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 964c68ab182..fdbba4212f6 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.18"], + "requirements": ["dsmr_parser==0.23"], "codeowners": ["@Robbie1221"], "config_flow": false } diff --git a/requirements_all.txt b/requirements_all.txt index f4905beb679..17034842c29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,7 +511,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.18 +dsmr_parser==0.23 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7160913d7af..b2025493aaa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ distro==1.5.0 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.18 +dsmr_parser==0.23 # homeassistant.components.dynalite dynalite_devices==0.1.46 From 0f823b4c0e023cf24ff0d309bdb7b4ca870da629 Mon Sep 17 00:00:00 2001 From: Aleix Murtra Date: Fri, 20 Nov 2020 19:16:18 +0100 Subject: [PATCH 154/430] Upgrade Beewi Smartclim component to 0.0.10 (#43441) --- homeassistant/components/beewi_smartclim/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 169132515d2..29f70b11352 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -2,6 +2,6 @@ "domain": "beewi_smartclim", "name": "BeeWi SmartClim BLE sensor", "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", - "requirements": ["beewi_smartclim==0.0.7"], + "requirements": ["beewi_smartclim==0.0.10"], "codeowners": ["@alemuro"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17034842c29..1583a5327eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ batinfo==0.4.2 beautifulsoup4==4.9.1 # homeassistant.components.beewi_smartclim -# beewi_smartclim==0.0.7 +# beewi_smartclim==0.0.10 # homeassistant.components.zha bellows==0.20.3 From 700336258b8b1d77902cb0cd996b80f304919220 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 20 Nov 2020 20:40:34 +0100 Subject: [PATCH 155/430] Fix empty local_ip config flow (#43333) --- homeassistant/components/local_ip/strings.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json index 1859223e657..7e214df2592 100644 --- a/homeassistant/components/local_ip/strings.json +++ b/homeassistant/components/local_ip/strings.json @@ -4,9 +4,7 @@ "step": { "user": { "title": "Local IP Address", - "data": { - "name": "Sensor Name" - } + "description": "[%key:common::config_flow::description::confirm_setup%]" } }, "abort": { From dd4f41c1dbed0f8c00cd80d89da35b8bde841b72 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 20 Nov 2020 21:04:03 +0100 Subject: [PATCH 156/430] Use weather attribute conditions constants (#39945) --- homeassistant/components/accuweather/const.py | 44 ++++++++---- .../components/buienradar/weather.py | 42 +++++++---- homeassistant/components/darksky/weather.py | 35 ++++++---- homeassistant/components/demo/weather.py | 70 +++++++++++-------- homeassistant/components/ecobee/const.py | 48 ++++++++----- .../components/environment_canada/weather.py | 36 ++++++---- .../components/homematicip_cloud/weather.py | 48 ++++++++----- homeassistant/components/ipma/weather.py | 42 +++++++---- .../components/meteo_france/const.py | 51 ++++++++++---- homeassistant/components/metoffice/const.py | 45 ++++++++---- .../components/openweathermap/const.py | 55 +++++++++++---- homeassistant/components/smhi/weather.py | 42 +++++++---- 12 files changed, 372 insertions(+), 186 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index aac37604584..fa9ed6b467f 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -1,4 +1,20 @@ """Constants for AccuWeather integration.""" +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -29,20 +45,20 @@ NAME = "AccuWeather" UNDO_UPDATE_LISTENER = "undo_update_listener" CONDITION_CLASSES = { - "clear-night": [33, 34, 37], - "cloudy": [7, 8, 38], - "exceptional": [24, 30, 31], - "fog": [11], - "hail": [25], - "lightning": [15], - "lightning-rainy": [16, 17, 41, 42], - "partlycloudy": [4, 6, 35, 36], - "pouring": [18], - "rainy": [12, 13, 14, 26, 39, 40], - "snowy": [19, 20, 21, 22, 23, 43, 44], - "snowy-rainy": [29], - "sunny": [1, 2, 3, 5], - "windy": [32], + ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], + ATTR_CONDITION_CLOUDY: [7, 8, 38], + ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31], + ATTR_CONDITION_FOG: [11], + ATTR_CONDITION_HAIL: [25], + ATTR_CONDITION_LIGHTNING: [15], + ATTR_CONDITION_LIGHTNING_RAINY: [16, 17, 41, 42], + ATTR_CONDITION_PARTLYCLOUDY: [4, 6, 35, 36], + ATTR_CONDITION_POURING: [18], + ATTR_CONDITION_RAINY: [12, 13, 14, 26, 39, 40], + ATTR_CONDITION_SNOWY: [19, 20, 21, 22, 23, 43, 44], + ATTR_CONDITION_SNOWY_RAINY: [29], + ATTR_CONDITION_SUNNY: [1, 2, 3, 5], + ATTR_CONDITION_WINDY: [32], } FORECAST_DAYS = [0, 1, 2, 3, 4] diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index d0a0c0e18b4..4b0391c3190 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -14,6 +14,20 @@ from buienradar.constants import ( import voluptuous as vol from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -40,20 +54,20 @@ CONF_FORECAST = "forecast" CONDITION_CLASSES = { - "cloudy": ["c", "p"], - "fog": ["d", "n"], - "hail": [], - "lightning": ["g"], - "lightning-rainy": ["s"], - "partlycloudy": ["b", "j", "o", "r"], - "pouring": ["l", "q"], - "rainy": ["f", "h", "k", "m"], - "snowy": ["u", "i", "v", "t"], - "snowy-rainy": ["w"], - "sunny": ["a"], - "windy": [], - "windy-variant": [], - "exceptional": [], + ATTR_CONDITION_CLOUDY: ["c", "p"], + ATTR_CONDITION_FOG: ["d", "n"], + ATTR_CONDITION_HAIL: [], + ATTR_CONDITION_LIGHTNING: ["g"], + ATTR_CONDITION_LIGHTNING_RAINY: ["s"], + ATTR_CONDITION_PARTLYCLOUDY: ["b", "j", "o", "r"], + ATTR_CONDITION_POURING: ["l", "q"], + ATTR_CONDITION_RAINY: ["f", "h", "k", "m"], + ATTR_CONDITION_SNOWY: ["u", "i", "v", "t"], + ATTR_CONDITION_SNOWY_RAINY: ["w"], + ATTR_CONDITION_SUNNY: ["a"], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index fee7d60a2c3..0ad448ddfbd 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -7,6 +7,17 @@ from requests.exceptions import ConnectionError as ConnectError, HTTPError, Time import voluptuous as vol from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -40,18 +51,18 @@ ATTRIBUTION = "Powered by Dark Sky" FORECAST_MODE = ["hourly", "daily"] MAP_CONDITION = { - "clear-day": "sunny", - "clear-night": "clear-night", - "rain": "rainy", - "snow": "snowy", - "sleet": "snowy-rainy", - "wind": "windy", - "fog": "fog", - "cloudy": "cloudy", - "partly-cloudy-day": "partlycloudy", - "partly-cloudy-night": "partlycloudy", - "hail": "hail", - "thunderstorm": "lightning", + "clear-day": ATTR_CONDITION_SUNNY, + "clear-night": ATTR_CONDITION_CLEAR_NIGHT, + "rain": ATTR_CONDITION_RAINY, + "snow": ATTR_CONDITION_SNOWY, + "sleet": ATTR_CONDITION_SNOWY_RAINY, + "wind": ATTR_CONDITION_WINDY, + "fog": ATTR_CONDITION_FOG, + "cloudy": ATTR_CONDITION_CLOUDY, + "partly-cloudy-day": ATTR_CONDITION_PARTLYCLOUDY, + "partly-cloudy-night": ATTR_CONDITION_PARTLYCLOUDY, + "hail": ATTR_CONDITION_HAIL, + "thunderstorm": ATTR_CONDITION_LIGHTNING, "tornado": None, } diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index 3c87cd1c27c..9792a45bbdf 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -2,6 +2,20 @@ from datetime import timedelta from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, @@ -14,20 +28,20 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT import homeassistant.util.dt as dt_util CONDITION_CLASSES = { - "cloudy": [], - "fog": [], - "hail": [], - "lightning": [], - "lightning-rainy": [], - "partlycloudy": [], - "pouring": [], - "rainy": ["shower rain"], - "snowy": [], - "snowy-rainy": [], - "sunny": ["sunshine"], - "windy": [], - "windy-variant": [], - "exceptional": [], + ATTR_CONDITION_CLOUDY: [], + ATTR_CONDITION_FOG: [], + ATTR_CONDITION_HAIL: [], + ATTR_CONDITION_LIGHTNING: [], + ATTR_CONDITION_LIGHTNING_RAINY: [], + ATTR_CONDITION_PARTLYCLOUDY: [], + ATTR_CONDITION_POURING: [], + ATTR_CONDITION_RAINY: ["shower rain"], + ATTR_CONDITION_SNOWY: [], + ATTR_CONDITION_SNOWY_RAINY: [], + ATTR_CONDITION_SUNNY: ["sunshine"], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], } @@ -49,13 +63,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 0.5, TEMP_CELSIUS, [ - ["rainy", 1, 22, 15, 60], - ["rainy", 5, 19, 8, 30], - ["cloudy", 0, 15, 9, 10], - ["sunny", 0, 12, 6, 0], - ["partlycloudy", 2, 14, 7, 20], - ["rainy", 15, 18, 7, 0], - ["fog", 0.2, 21, 12, 100], + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], ], ), DemoWeather( @@ -67,13 +81,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 4.8, TEMP_FAHRENHEIT, [ - ["snowy", 2, -10, -15, 60], - ["partlycloudy", 1, -13, -14, 25], - ["sunny", 0, -18, -22, 70], - ["sunny", 0.1, -23, -23, 90], - ["snowy", 4, -19, -20, 40], - ["sunny", 0.3, -14, -19, 0], - ["sunny", 0, -9, -12, 0], + [ATTR_CONDITION_SNOWY, 2, -10, -15, 60], + [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], + [ATTR_CONDITION_SUNNY, 0, -18, -22, 70], + [ATTR_CONDITION_SUNNY, 0.1, -23, -23, 90], + [ATTR_CONDITION_SNOWY, 4, -19, -20, 40], + [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0], + [ATTR_CONDITION_SUNNY, 0, -9, -12, 0], ], ), ] diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index f380c9bbef3..5ec3a0fcf96 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -1,6 +1,20 @@ """Constants for the ecobee integration.""" import logging +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, +) + _LOGGER = logging.getLogger(__package__) DOMAIN = "ecobee" @@ -30,25 +44,25 @@ MANUFACTURER = "ecobee" # Translates ecobee API weatherSymbol to Home Assistant 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", + 0: ATTR_CONDITION_SUNNY, + 1: ATTR_CONDITION_PARTLYCLOUDY, + 2: ATTR_CONDITION_PARTLYCLOUDY, + 3: ATTR_CONDITION_CLOUDY, + 4: ATTR_CONDITION_CLOUDY, + 5: ATTR_CONDITION_CLOUDY, + 6: ATTR_CONDITION_RAINY, + 7: ATTR_CONDITION_SNOWY_RAINY, + 8: ATTR_CONDITION_POURING, + 9: ATTR_CONDITION_HAIL, + 10: ATTR_CONDITION_SNOWY, + 11: ATTR_CONDITION_SNOWY, + 12: ATTR_CONDITION_SNOWY_RAINY, 13: "snowy-heavy", - 14: "hail", - 15: "lightning-rainy", - 16: "windy", + 14: ATTR_CONDITION_HAIL, + 15: ATTR_CONDITION_LIGHTNING_RAINY, + 16: ATTR_CONDITION_WINDY, 17: "tornado", - 18: "fog", + 18: ATTR_CONDITION_FOG, 19: "hazy", 20: "hazy", 21: "hazy", diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 3c3dbfe6ce6..f4fa96b52d6 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -6,6 +6,18 @@ from env_canada import ECData # pylint: disable=import-error import voluptuous as vol from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -45,18 +57,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/ # docs/current_conditions_icon_code_descriptions_e.csv ICON_CONDITION_MAP = { - "sunny": [0, 1], - "clear-night": [30, 31], - "partlycloudy": [2, 3, 4, 5, 22, 32, 33, 34, 35], - "cloudy": [10], - "rainy": [6, 9, 11, 12, 28, 36], - "lightning-rainy": [19, 39, 46, 47], - "pouring": [13], - "snowy-rainy": [7, 14, 15, 27, 37], - "snowy": [8, 16, 17, 18, 25, 26, 38, 40], - "windy": [43], - "fog": [20, 21, 23, 24, 44], - "hail": [26, 27], + ATTR_CONDITION_SUNNY: [0, 1], + ATTR_CONDITION_CLEAR_NIGHT: [30, 31], + ATTR_CONDITION_PARTLYCLOUDY: [2, 3, 4, 5, 22, 32, 33, 34, 35], + ATTR_CONDITION_CLOUDY: [10], + ATTR_CONDITION_RAINY: [6, 9, 11, 12, 28, 36], + ATTR_CONDITION_LIGHTNING_RAINY: [19, 39, 46, 47], + ATTR_CONDITION_POURING: [13], + ATTR_CONDITION_SNOWY_RAINY: [7, 14, 15, 27, 37], + ATTR_CONDITION_SNOWY: [8, 16, 17, 18, 25, 26, 38, 40], + ATTR_CONDITION_WINDY: [43], + ATTR_CONDITION_FOG: [20, 21, 23, 24, 44], + ATTR_CONDITION_HAIL: [26, 27], } diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index b880cfdf1cc..bdfd505a317 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -6,7 +6,19 @@ from homematicip.aio.device import ( ) from homematicip.base.enums import WeatherCondition -from homeassistant.components.weather import WeatherEntity +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType @@ -15,20 +27,20 @@ from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP 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.CLEAR: ATTR_CONDITION_SUNNY, + WeatherCondition.LIGHT_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, + WeatherCondition.CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCondition.CLOUDY_WITH_RAIN: ATTR_CONDITION_RAINY, + WeatherCondition.CLOUDY_WITH_SNOW_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCondition.HEAVILY_CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: ATTR_CONDITION_RAINY, + WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: ATTR_CONDITION_SNOWY, + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: ATTR_CONDITION_LIGHTNING, + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: ATTR_CONDITION_LIGHTNING_RAINY, + WeatherCondition.FOGGY: ATTR_CONDITION_FOG, + WeatherCondition.STRONG_WIND: ATTR_CONDITION_WINDY, WeatherCondition.UNKNOWN: "", } @@ -92,11 +104,11 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): def condition(self) -> str: """Return the current condition.""" if getattr(self._device, "raining", None): - return "rainy" + return ATTR_CONDITION_RAINY if self._device.storm: - return "windy" + return ATTR_CONDITION_WINDY if self._device.sunshine: - return "sunny" + return ATTR_CONDITION_SUNNY return "" diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index d8ac0c039da..32a5967f8b4 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -8,6 +8,20 @@ from pyipma.location import Location import voluptuous as vol from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -38,20 +52,20 @@ ATTRIBUTION = "Instituto Português do Mar e Atmosfera" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) CONDITION_CLASSES = { - "cloudy": [4, 5, 24, 25, 27], - "fog": [16, 17, 26], - "hail": [21, 22], - "lightning": [19], - "lightning-rainy": [20, 23], - "partlycloudy": [2, 3], - "pouring": [8, 11], - "rainy": [6, 7, 9, 10, 12, 13, 14, 15], - "snowy": [18], - "snowy-rainy": [], - "sunny": [1], - "windy": [], - "windy-variant": [], - "exceptional": [], + ATTR_CONDITION_CLOUDY: [4, 5, 24, 25, 27], + ATTR_CONDITION_FOG: [16, 17, 26], + ATTR_CONDITION_HAIL: [21, 22], + ATTR_CONDITION_LIGHTNING: [19], + ATTR_CONDITION_LIGHTNING_RAINY: [20, 23], + ATTR_CONDITION_PARTLYCLOUDY: [2, 3], + ATTR_CONDITION_POURING: [8, 11], + ATTR_CONDITION_RAINY: [6, 7, 9, 10, 12, 13, 14, 15], + ATTR_CONDITION_SNOWY: [18], + ATTR_CONDITION_SNOWY_RAINY: [], + ATTR_CONDITION_SUNNY: [1], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], } FORECAST_MODE = ["hourly", "daily"] diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 3ae24cbc71b..bfbaa828ea7 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -1,5 +1,22 @@ """Meteo-France component constants.""" +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, +) from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -126,27 +143,31 @@ SENSOR_TYPES = { } CONDITION_CLASSES = { - "clear-night": ["Nuit Claire", "Nuit claire"], - "cloudy": ["Très nuageux", "Couvert"], - "fog": [ + ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"], + ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"], + ATTR_CONDITION_FOG: [ "Brume ou bancs de brouillard", "Brume", "Brouillard", "Brouillard givrant", "Bancs de Brouillard", ], - "hail": ["Risque de grêle", "Risque de grèle"], - "lightning": ["Risque d'orages", "Orages"], - "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], - "partlycloudy": [ + ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle"], + ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages"], + ATTR_CONDITION_LIGHTNING_RAINY: [ + "Pluie orageuses", + "Pluies orageuses", + "Averses orageuses", + ], + ATTR_CONDITION_PARTLYCLOUDY: [ "Ciel voilé", "Ciel voilé nuit", "Éclaircies", "Eclaircies", "Peu nuageux", ], - "pouring": ["Pluie forte"], - "rainy": [ + ATTR_CONDITION_POURING: ["Pluie forte"], + ATTR_CONDITION_RAINY: [ "Bruine / Pluie faible", "Bruine", "Pluie faible", @@ -158,16 +179,16 @@ CONDITION_CLASSES = { "Averses", "Pluie", ], - "snowy": [ + ATTR_CONDITION_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": [], + ATTR_CONDITION_SNOWY_RAINY: ["Pluie et neige", "Pluie verglaçante"], + ATTR_CONDITION_SUNNY: ["Ensoleillé"], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], } diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index b088672b8a5..e710911ee59 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -1,6 +1,23 @@ """Constants for Met Office Integration.""" from datetime import timedelta +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, +) + DOMAIN = "metoffice" DEFAULT_NAME = "Met Office" @@ -16,20 +33,20 @@ METOFFICE_NAME = "metoffice_name" MODE_3HOURLY = "3hourly" CONDITION_CLASSES = { - "cloudy": ["7", "8"], - "fog": ["5", "6"], - "hail": ["19", "20", "21"], - "lightning": ["30"], - "lightning-rainy": ["28", "29"], - "partlycloudy": ["2", "3"], - "pouring": ["13", "14", "15"], - "rainy": ["9", "10", "11", "12"], - "snowy": ["22", "23", "24", "25", "26", "27"], - "snowy-rainy": ["16", "17", "18"], - "sunny": ["0", "1"], - "windy": [], - "windy-variant": [], - "exceptional": [], + ATTR_CONDITION_CLOUDY: ["7", "8"], + ATTR_CONDITION_FOG: ["5", "6"], + ATTR_CONDITION_HAIL: ["19", "20", "21"], + ATTR_CONDITION_LIGHTNING: ["30"], + ATTR_CONDITION_LIGHTNING_RAINY: ["28", "29"], + ATTR_CONDITION_PARTLYCLOUDY: ["2", "3"], + ATTR_CONDITION_POURING: ["13", "14", "15"], + ATTR_CONDITION_RAINY: ["9", "10", "11", "12"], + ATTR_CONDITION_SNOWY: ["22", "23", "24", "25", "26", "27"], + ATTR_CONDITION_SNOWY_RAINY: ["16", "17", "18"], + ATTR_CONDITION_SUNNY: ["0", "1"], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], } VISIBILITY_CLASSES = { diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 2c23c618c75..36f51a99ee1 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -1,5 +1,19 @@ """Consts for the OpenWeatherMap.""" from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -136,20 +150,33 @@ LANGUAGES = [ "zu", ] CONDITION_CLASSES = { - "cloudy": [803, 804], - "fog": [701, 741], - "hail": [906], - "lightning": [210, 211, 212, 221], - "lightning-rainy": [200, 201, 202, 230, 231, 232], - "partlycloudy": [801, 802], - "pouring": [504, 314, 502, 503, 522], - "rainy": [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], - "snowy": [600, 601, 602, 611, 612, 620, 621, 622], - "snowy-rainy": [511, 615, 616], - "sunny": [800], - "windy": [905, 951, 952, 953, 954, 955, 956, 957], - "windy-variant": [958, 959, 960, 961], - "exceptional": [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, 904], + ATTR_CONDITION_CLOUDY: [803, 804], + ATTR_CONDITION_FOG: [701, 741], + ATTR_CONDITION_HAIL: [906], + ATTR_CONDITION_LIGHTNING: [210, 211, 212, 221], + ATTR_CONDITION_LIGHTNING_RAINY: [200, 201, 202, 230, 231, 232], + ATTR_CONDITION_PARTLYCLOUDY: [801, 802], + ATTR_CONDITION_POURING: [504, 314, 502, 503, 522], + ATTR_CONDITION_RAINY: [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], + ATTR_CONDITION_SNOWY: [600, 601, 602, 611, 612, 620, 621, 622], + ATTR_CONDITION_SNOWY_RAINY: [511, 615, 616], + ATTR_CONDITION_SUNNY: [800], + ATTR_CONDITION_WINDY: [905, 951, 952, 953, 954, 955, 956, 957], + ATTR_CONDITION_WINDY_VARIANT: [958, 959, 960, 961], + ATTR_CONDITION_EXCEPTIONAL: [ + 711, + 721, + 731, + 751, + 761, + 762, + 771, + 900, + 901, + 962, + 903, + 904, + ], } WEATHER_SENSOR_TYPES = { ATTR_API_WEATHER: {SENSOR_NAME: "Weather"}, diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index f09491bf611..c13982ee15d 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -10,6 +10,20 @@ from smhi import Smhi from smhi.smhi_lib import SmhiForecastException from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -29,20 +43,20 @@ _LOGGER = logging.getLogger(__name__) # Used to map condition from API results CONDITION_CLASSES = { - "cloudy": [5, 6], - "fog": [7], - "hail": [], - "lightning": [21], - "lightning-rainy": [11], - "partlycloudy": [3, 4], - "pouring": [10, 20], - "rainy": [8, 9, 18, 19], - "snowy": [15, 16, 17, 25, 26, 27], - "snowy-rainy": [12, 13, 14, 22, 23, 24], - "sunny": [1, 2], - "windy": [], - "windy-variant": [], - "exceptional": [], + ATTR_CONDITION_CLOUDY: [5, 6], + ATTR_CONDITION_FOG: [7], + ATTR_CONDITION_HAIL: [], + ATTR_CONDITION_LIGHTNING: [21], + ATTR_CONDITION_LIGHTNING_RAINY: [11], + ATTR_CONDITION_PARTLYCLOUDY: [3, 4], + ATTR_CONDITION_POURING: [10, 20], + ATTR_CONDITION_RAINY: [8, 9, 18, 19], + ATTR_CONDITION_SNOWY: [15, 16, 17, 25, 26, 27], + ATTR_CONDITION_SNOWY_RAINY: [12, 13, 14, 22, 23, 24], + ATTR_CONDITION_SUNNY: [1, 2], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], } # 5 minutes between retrying connect to API again From a880ef6a4e354eeb533765950b17f7756443ab38 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Nov 2020 13:14:44 -0700 Subject: [PATCH 157/430] Move Flo logger to a package logger (#43449) --- homeassistant/components/flo/config_flow.py | 8 ++------ homeassistant/components/flo/const.py | 4 ++++ homeassistant/components/flo/device.py | 11 ++++------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py index b509c894068..54c6ae94ee2 100644 --- a/homeassistant/components/flo/config_flow.py +++ b/homeassistant/components/flo/config_flow.py @@ -1,6 +1,4 @@ """Config flow for flo integration.""" -import logging - from aioflo import async_get_api from aioflo.errors import RequestError import voluptuous as vol @@ -9,9 +7,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint:disable=unused-import - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, LOGGER # pylint:disable=unused-import DATA_SCHEMA = vol.Schema({"username": str, "password": str}) @@ -28,7 +24,7 @@ async def validate_input(hass: core.HomeAssistant, data): data[CONF_USERNAME], data[CONF_PASSWORD], session=session ) except RequestError as request_error: - _LOGGER.error("Error connecting to the Flo API: %s", request_error) + LOGGER.error("Error connecting to the Flo API: %s", request_error) raise CannotConnect from request_error user_info = await api.user.get_info() diff --git a/homeassistant/components/flo/const.py b/homeassistant/components/flo/const.py index 94c1b8d4579..907561b5b9c 100644 --- a/homeassistant/components/flo/const.py +++ b/homeassistant/components/flo/const.py @@ -1,4 +1,8 @@ """Constants for the flo integration.""" +import logging + +LOGGER = logging.getLogger(__package__) + CLIENT = "client" DOMAIN = "flo" FLO_HOME = "home" diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 824d62a9519..af36034026d 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -1,7 +1,6 @@ """Flo device object.""" import asyncio from datetime import datetime, timedelta -import logging from typing import Any, Dict, Optional from aioflo.api import API @@ -12,9 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util -from .const import DOMAIN as FLO_DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN as FLO_DOMAIN, LOGGER class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): @@ -33,7 +30,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): self._water_usage: Optional[Dict[str, Any]] = None super().__init__( hass, - _LOGGER, + LOGGER, name=f"{FLO_DOMAIN}-{device_id}", update_interval=timedelta(seconds=60), ) @@ -195,7 +192,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): self._device_information = await self.api_client.device.get_info( self._flo_device_id ) - _LOGGER.debug("Flo device data: %s", self._device_information) + LOGGER.debug("Flo device data: %s", self._device_information) async def _update_consumption_data(self, *_) -> None: """Update water consumption data from the API.""" @@ -205,4 +202,4 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): self._water_usage = await self.api_client.water.get_consumption_info( self._flo_location_id, start_date, end_date ) - _LOGGER.debug("Updated Flo consumption data: %s", self._water_usage) + LOGGER.debug("Updated Flo consumption data: %s", self._water_usage) From 87795929525b0a89bc316543adbfe83aae48d41c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Nov 2020 13:20:56 -0700 Subject: [PATCH 158/430] Move Ambient PWS logger to a package logger (#43448) --- .../components/ambient_station/__init__.py | 20 +++++++++---------- .../components/ambient_station/const.py | 3 +++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9fabed7c30a..4a5558c5963 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,6 +1,5 @@ """Support for Ambient Weather Station Service.""" import asyncio -import logging from aioambient import Client from aioambient.errors import WebsocketError @@ -39,12 +38,11 @@ from .const import ( CONF_APP_KEY, DATA_CLIENT, DOMAIN, + LOGGER, TYPE_BINARY_SENSOR, TYPE_SENSOR, ) -_LOGGER = logging.getLogger(__name__) - DATA_CONFIG = "config" DEFAULT_SOCKET_MIN_RETRY = 15 @@ -307,7 +305,7 @@ async def async_setup_entry(hass, config_entry): hass.loop.create_task(ambient.ws_connect()) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient except WebsocketError as err: - _LOGGER.error("Config entry failed: %s", err) + LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err async def _async_disconnect_websocket(*_): @@ -337,7 +335,7 @@ async def async_migrate_entry(hass, config_entry): """Migrate old entry.""" version = config_entry.version - _LOGGER.debug("Migrating from version %s", version) + LOGGER.debug("Migrating from version %s", version) # 1 -> 2: Unique ID format changed, so delete and re-import: if version == 1: @@ -350,7 +348,7 @@ async def async_migrate_entry(hass, config_entry): version = config_entry.version = 2 hass.config_entries.async_update_entry(config_entry) - _LOGGER.info("Migration to version %s successful", version) + LOGGER.info("Migration to version %s successful", version) return True @@ -377,7 +375,7 @@ class AmbientStation: try: await connect() except WebsocketError as err: - _LOGGER.error("Error with the websocket connection: %s", err) + LOGGER.error("Error with the websocket connection: %s", err) self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) async_call_later(self._hass, self._ws_reconnect_delay, connect) @@ -386,13 +384,13 @@ class AmbientStation: def on_connect(): """Define a handler to fire when the websocket is connected.""" - _LOGGER.info("Connected to websocket") + LOGGER.info("Connected to websocket") def on_data(data): """Define a handler to fire when the data is received.""" mac_address = data["macAddress"] if data != self.stations[mac_address][ATTR_LAST_DATA]: - _LOGGER.debug("New data received: %s", data) + LOGGER.debug("New data received: %s", data) self.stations[mac_address][ATTR_LAST_DATA] = data async_dispatcher_send( self._hass, f"ambient_station_data_update_{mac_address}" @@ -400,7 +398,7 @@ class AmbientStation: def on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" - _LOGGER.info("Disconnected from websocket") + LOGGER.info("Disconnected from websocket") def on_subscribed(data): """Define a handler to fire when the subscription is set.""" @@ -408,7 +406,7 @@ class AmbientStation: if station["macAddress"] in self.stations: continue - _LOGGER.debug("New station subscription: %s", data) + LOGGER.debug("New station subscription: %s", data) # Only create entities based on the data coming through the socket. # If the user is monitoring brightness (in W/m^2), make sure we also diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index 3b1990ae837..e59f926eac3 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -1,5 +1,8 @@ """Define constants for the Ambient PWS component.""" +import logging + DOMAIN = "ambient_station" +LOGGER = logging.getLogger(__package__) ATTR_LAST_DATA = "last_data" ATTR_MONITORED_CONDITIONS = "monitored_conditions" From a74559471289ec605348990e265de2e7c8bb3d7e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 20 Nov 2020 21:28:00 +0100 Subject: [PATCH 159/430] Move legacy device tracker setup to legacy module (#43447) --- .../components/device_tracker/__init__.py | 147 +------- .../components/device_tracker/legacy.py | 337 +++++++++++++++++- .../components/device_tracker/setup.py | 196 ---------- 3 files changed, 341 insertions(+), 339 deletions(-) delete mode 100644 homeassistant/components/device_tracker/setup.py diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6d8e2307145..d785ee826e8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,24 +1,18 @@ """Provide functionality to keep track of devices.""" -import asyncio - -import voluptuous as vol - -from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import + ATTR_GPS_ACCURACY, + STATE_HOME, +) +from homeassistant.helpers.typing import ConfigType, 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 .const import ( +from .const import ( # noqa: F401 pylint: disable=unused-import ATTR_ATTRIBUTES, ATTR_BATTERY, - ATTR_CONSIDER_HOME, ATTR_DEV_ID, ATTR_GPS, ATTR_HOST_NAME, @@ -29,60 +23,21 @@ from .const import ( CONF_NEW_DEVICE_DEFAULTS, CONF_SCAN_INTERVAL, CONF_TRACK_NEW, - DEFAULT_CONSIDER_HOME, - DEFAULT_TRACK_NEW, DOMAIN, - PLATFORM_TYPE_LEGACY, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, ) -from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import - -SERVICE_SEE = "see" - -SOURCE_TYPES = ( - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, -) - -NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( - None, - vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}), -) -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_TRACK_NEW): cv.boolean, - vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, - } -) -PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) -SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), - { - ATTR_MAC: cv.string, - ATTR_DEV_ID: cv.string, - ATTR_HOST_NAME: cv.string, - ATTR_LOCATION_NAME: cv.string, - ATTR_GPS: cv.gps, - ATTR_GPS_ACCURACY: cv.positive_int, - ATTR_BATTERY: cv.positive_int, - ATTR_ATTRIBUTES: dict, - ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), - ATTR_CONSIDER_HOME: cv.time_period, - # Temp workaround for iOS app introduced in 0.65 - vol.Optional("battery_status"): str, - vol.Optional("hostname"): str, - }, - ) +from .legacy import ( # noqa: F401 pylint: disable=unused-import + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + SERVICE_SEE, + SERVICE_SEE_PAYLOAD_SCHEMA, + SOURCE_TYPES, + DeviceScanner, + async_setup_integration as async_setup_legacy_integration, + see, ) @@ -92,78 +47,8 @@ def is_on(hass: HomeAssistantType, entity_id: str): return hass.states.is_state(entity_id, STATE_HOME) -def see( - hass: HomeAssistantType, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, - gps_accuracy=None, - battery: int = None, - attributes: dict = None, -): - """Call service to notify you see device.""" - data = { - key: value - for key, value in ( - (ATTR_MAC, mac), - (ATTR_DEV_ID, dev_id), - (ATTR_HOST_NAME, host_name), - (ATTR_LOCATION_NAME, location_name), - (ATTR_GPS, gps), - (ATTR_GPS_ACCURACY, gps_accuracy), - (ATTR_BATTERY, battery), - ) - if value is not None - } - if attributes: - data[ATTR_ATTRIBUTES] = attributes - hass.services.call(DOMAIN, SERVICE_SEE, data) - - async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" - tracker = await legacy.get_tracker(hass, config) + await async_setup_legacy_integration(hass, config) - legacy_platforms = await setup.async_extract_config(hass, config) - - setup_tasks = [ - legacy_platform.async_setup_legacy(hass, tracker) - for legacy_platform in legacy_platforms - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) - - async def async_platform_discovered(p_type, info): - """Load a platform.""" - platform = await setup.async_create_platform_type(hass, config, p_type, {}) - - if platform is None or platform.type != PLATFORM_TYPE_LEGACY: - return - - await platform.async_setup_legacy(hass, tracker, info) - - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) - - # Clean up stale devices - async_track_utc_time_change( - hass, tracker.async_update_stale, second=range(0, 60, 5) - ) - - async def async_see_service(call): - """Service to see a device.""" - # Temp workaround for iOS, introduced in 0.65 - data = dict(call.data) - data.pop("hostname", None) - data.pop("battery_status", None) - await tracker.async_see(**data) - - hass.services.async_register( - DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA - ) - - # restore - await tracker.async_setup_tracked_device() return True diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 038fad06680..5f60d84f406 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -2,8 +2,10 @@ import asyncio from datetime import timedelta import hashlib -from typing import Any, List, Sequence +from types import ModuleType +from typing import Any, Callable, Dict, List, Optional, Sequence +import attr import voluptuous as vol from homeassistant import util @@ -25,32 +27,343 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.event import ( + async_track_time_interval, + async_track_utc_time_change, +) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import GPSType, HomeAssistantType -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util import dt as dt_util from homeassistant.util.yaml import dump from .const import ( + ATTR_ATTRIBUTES, ATTR_BATTERY, + ATTR_CONSIDER_HOME, + ATTR_DEV_ID, + ATTR_GPS, ATTR_HOST_NAME, + ATTR_LOCATION_NAME, ATTR_MAC, ATTR_SOURCE_TYPE, CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, + CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_CONSIDER_HOME, DEFAULT_TRACK_NEW, DOMAIN, LOGGER, + PLATFORM_TYPE_LEGACY, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, + SOURCE_TYPE_ROUTER, +) + +SERVICE_SEE = "see" + +SOURCE_TYPES = ( + SOURCE_TYPE_GPS, + SOURCE_TYPE_ROUTER, + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, +) + +NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( + None, + vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}), +) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, + } +) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) + +SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( + vol.All( + cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), + { + ATTR_MAC: cv.string, + ATTR_DEV_ID: cv.string, + ATTR_HOST_NAME: cv.string, + ATTR_LOCATION_NAME: cv.string, + ATTR_GPS: cv.gps, + ATTR_GPS_ACCURACY: cv.positive_int, + ATTR_BATTERY: cv.positive_int, + ATTR_ATTRIBUTES: dict, + ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), + ATTR_CONSIDER_HOME: cv.time_period, + # Temp workaround for iOS app introduced in 0.65 + vol.Optional("battery_status"): str, + vol.Optional("hostname"): str, + }, + ) ) YAML_DEVICES = "known_devices.yaml" EVENT_NEW_DEVICE = "device_tracker_new_device" +def see( + hass: HomeAssistantType, + mac: str = None, + dev_id: str = None, + host_name: str = None, + location_name: str = None, + gps: GPSType = None, + gps_accuracy=None, + battery: int = None, + attributes: dict = None, +): + """Call service to notify you see device.""" + data = { + key: value + for key, value in ( + (ATTR_MAC, mac), + (ATTR_DEV_ID, dev_id), + (ATTR_HOST_NAME, host_name), + (ATTR_LOCATION_NAME, location_name), + (ATTR_GPS, gps), + (ATTR_GPS_ACCURACY, gps_accuracy), + (ATTR_BATTERY, battery), + ) + if value is not None + } + if attributes: + data[ATTR_ATTRIBUTES] = attributes + hass.services.call(DOMAIN, SERVICE_SEE, data) + + +async def async_setup_integration(hass: HomeAssistantType, config: ConfigType) -> None: + """Set up the legacy integration.""" + tracker = await get_tracker(hass, config) + + legacy_platforms = await async_extract_config(hass, config) + + setup_tasks = [ + legacy_platform.async_setup_legacy(hass, tracker) + for legacy_platform in legacy_platforms + ] + + if setup_tasks: + await asyncio.wait(setup_tasks) + + async def async_platform_discovered(p_type, info): + """Load a platform.""" + platform = await async_create_platform_type(hass, config, p_type, {}) + + if platform is None or platform.type != PLATFORM_TYPE_LEGACY: + return + + await platform.async_setup_legacy(hass, tracker, info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + + # Clean up stale devices + async_track_utc_time_change( + hass, tracker.async_update_stale, second=range(0, 60, 5) + ) + + async def async_see_service(call): + """Service to see a device.""" + # Temp workaround for iOS, introduced in 0.65 + data = dict(call.data) + data.pop("hostname", None) + data.pop("battery_status", None) + await tracker.async_see(**data) + + hass.services.async_register( + DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA + ) + + # restore + await tracker.async_setup_tracked_device() + + +@attr.s +class DeviceTrackerPlatform: + """Class to hold platform information.""" + + LEGACY_SETUP = ( + "async_get_scanner", + "get_scanner", + "async_setup_scanner", + "setup_scanner", + ) + + name: str = attr.ib() + platform: ModuleType = attr.ib() + config: Dict = attr.ib() + + @property + def type(self): + """Return platform type.""" + for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): + for meth in methods: + if hasattr(self.platform, meth): + return platform_type + + return None + + async def async_setup_legacy(self, hass, tracker, discovery_info=None): + """Set up a legacy platform.""" + LOGGER.info("Setting up %s.%s", DOMAIN, self.type) + try: + scanner = None + setup = None + if hasattr(self.platform, "async_get_scanner"): + scanner = await self.platform.async_get_scanner( + hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "get_scanner"): + scanner = await hass.async_add_executor_job( + self.platform.get_scanner, hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "async_setup_scanner"): + setup = await self.platform.async_setup_scanner( + hass, self.config, tracker.async_see, discovery_info + ) + elif hasattr(self.platform, "setup_scanner"): + setup = await hass.async_add_executor_job( + self.platform.setup_scanner, + hass, + self.config, + tracker.see, + discovery_info, + ) + else: + raise HomeAssistantError("Invalid legacy device_tracker platform.") + + if scanner: + async_setup_scanner_platform( + hass, self.config, scanner, tracker.async_see, self.type + ) + return + + if not setup: + LOGGER.error("Error setting up platform %s", self.type) + return + + except Exception: # pylint: disable=broad-except + LOGGER.exception("Error setting up platform %s", self.type) + + +async def async_extract_config(hass, config): + """Extract device tracker config and split between legacy and modern.""" + legacy = [] + + for platform in await asyncio.gather( + *( + async_create_platform_type(hass, config, p_type, p_config) + for p_type, p_config in config_per_platform(config, DOMAIN) + ) + ): + if platform is None: + continue + + if platform.type == PLATFORM_TYPE_LEGACY: + legacy.append(platform) + else: + raise ValueError( + f"Unable to determine type for {platform.name}: {platform.type}" + ) + + return legacy + + +async def async_create_platform_type( + hass, config, p_type, p_config +) -> Optional[DeviceTrackerPlatform]: + """Determine type of platform.""" + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) + + if platform is None: + return None + + return DeviceTrackerPlatform(p_type, platform, p_config) + + +@callback +def async_setup_scanner_platform( + hass: HomeAssistantType, + config: ConfigType, + scanner: Any, + async_see_device: Callable, + platform: str, +): + """Set up the connect scanner-based platform to device tracker. + + This method must be run in the event loop. + """ + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + update_lock = asyncio.Lock() + scanner.hass = hass + + # Initial scan of each mac we also tell about host name for config + seen: Any = set() + + async def async_device_tracker_scan(now: dt_util.dt.datetime): + """Handle interval matches.""" + if update_lock.locked(): + LOGGER.warning( + "Updating device list from %s took longer than the scheduled " + "scan interval %s", + platform, + interval, + ) + return + + async with update_lock: + found_devices = await scanner.async_scan_devices() + + for mac in found_devices: + if mac in seen: + host_name = None + else: + host_name = await scanner.async_get_device_name(mac) + seen.add(mac) + + try: + extra_attributes = await scanner.async_get_extra_attributes(mac) + except NotImplementedError: + extra_attributes = {} + + kwargs = { + "mac": mac, + "host_name": host_name, + "source_type": SOURCE_TYPE_ROUTER, + "attributes": { + "scanner": scanner.__class__.__name__, + **extra_attributes, + }, + } + + zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) + if zone_home: + kwargs["gps"] = [ + zone_home.attributes[ATTR_LATITUDE], + zone_home.attributes[ATTR_LONGITUDE], + ] + kwargs["gps_accuracy"] = 0 + + hass.async_create_task(async_see_device(**kwargs)) + + async_track_time_interval(hass, async_device_tracker_scan, interval) + hass.async_create_task(async_device_tracker_scan(None)) + + async def get_tracker(hass, config): """Create a tracker.""" yaml_path = hass.config.path(YAML_DEVICES) @@ -349,17 +662,17 @@ class Device(RestoreEntity): @property def state_attributes(self): """Return the device state attributes.""" - attr = {ATTR_SOURCE_TYPE: self.source_type} + attributes = {ATTR_SOURCE_TYPE: self.source_type} if self.gps: - attr[ATTR_LATITUDE] = self.gps[0] - attr[ATTR_LONGITUDE] = self.gps[1] - attr[ATTR_GPS_ACCURACY] = self.gps_accuracy + attributes[ATTR_LATITUDE] = self.gps[0] + attributes[ATTR_LONGITUDE] = self.gps[1] + attributes[ATTR_GPS_ACCURACY] = self.gps_accuracy if self.battery: - attr[ATTR_BATTERY] = self.battery + attributes[ATTR_BATTERY] = self.battery - return attr + return attributes @property def device_state_attributes(self): @@ -453,13 +766,13 @@ class Device(RestoreEntity): self.last_update_home = state.state == STATE_HOME self.last_seen = dt_util.utcnow() - for attr, var in ( + for attribute, var in ( (ATTR_SOURCE_TYPE, "source_type"), (ATTR_GPS_ACCURACY, "gps_accuracy"), (ATTR_BATTERY, "battery"), ): - if attr in state.attributes: - setattr(self, var, state.attributes[attr]) + if attribute in state.attributes: + setattr(self, var, state.attributes[attribute]) if ATTR_LONGITUDE in state.attributes: self.gps = ( diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py deleted file mode 100644 index 133ea4eb414..00000000000 --- a/homeassistant/components/device_tracker/setup.py +++ /dev/null @@ -1,196 +0,0 @@ -"""Device tracker helpers.""" -import asyncio -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.exceptions import HomeAssistantError -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 .const import ( - CONF_SCAN_INTERVAL, - DOMAIN, - LOGGER, - PLATFORM_TYPE_LEGACY, - SCAN_INTERVAL, - SOURCE_TYPE_ROUTER, -) - - -@attr.s -class DeviceTrackerPlatform: - """Class to hold platform information.""" - - LEGACY_SETUP = ( - "async_get_scanner", - "get_scanner", - "async_setup_scanner", - "setup_scanner", - ) - - name: str = attr.ib() - platform: ModuleType = attr.ib() - config: Dict = attr.ib() - - @property - def type(self): - """Return platform type.""" - for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): - for meth in methods: - if hasattr(self.platform, meth): - return platform_type - - return None - - async def async_setup_legacy(self, hass, tracker, discovery_info=None): - """Set up a legacy platform.""" - LOGGER.info("Setting up %s.%s", DOMAIN, self.type) - try: - scanner = None - setup = None - if hasattr(self.platform, "async_get_scanner"): - scanner = await self.platform.async_get_scanner( - hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "get_scanner"): - scanner = await hass.async_add_executor_job( - self.platform.get_scanner, hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "async_setup_scanner"): - setup = await self.platform.async_setup_scanner( - hass, self.config, tracker.async_see, discovery_info - ) - elif hasattr(self.platform, "setup_scanner"): - setup = await hass.async_add_executor_job( - self.platform.setup_scanner, - hass, - self.config, - tracker.see, - discovery_info, - ) - else: - raise HomeAssistantError("Invalid legacy device_tracker platform.") - - if scanner: - async_setup_scanner_platform( - hass, self.config, scanner, tracker.async_see, self.type - ) - return - - if not setup: - LOGGER.error("Error setting up platform %s", self.type) - return - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Error setting up platform %s", self.type) - - -async def async_extract_config(hass, config): - """Extract device tracker config and split between legacy and modern.""" - legacy = [] - - for platform in await asyncio.gather( - *( - async_create_platform_type(hass, config, p_type, p_config) - for p_type, p_config in config_per_platform(config, DOMAIN) - ) - ): - if platform is None: - continue - - if platform.type == PLATFORM_TYPE_LEGACY: - legacy.append(platform) - else: - raise ValueError( - f"Unable to determine type for {platform.name}: {platform.type}" - ) - - return legacy - - -async def async_create_platform_type( - hass, config, p_type, p_config -) -> Optional[DeviceTrackerPlatform]: - """Determine type of platform.""" - platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) - - if platform is None: - return None - - return DeviceTrackerPlatform(p_type, platform, p_config) - - -@callback -def async_setup_scanner_platform( - hass: HomeAssistantType, - config: ConfigType, - scanner: Any, - async_see_device: Callable, - platform: str, -): - """Set up the connect scanner-based platform to device tracker. - - This method must be run in the event loop. - """ - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - update_lock = asyncio.Lock() - scanner.hass = hass - - # Initial scan of each mac we also tell about host name for config - seen: Any = set() - - async def async_device_tracker_scan(now: dt_util.dt.datetime): - """Handle interval matches.""" - if update_lock.locked(): - LOGGER.warning( - "Updating device list from %s took longer than the scheduled " - "scan interval %s", - platform, - interval, - ) - return - - async with update_lock: - found_devices = await scanner.async_scan_devices() - - for mac in found_devices: - if mac in seen: - host_name = None - else: - host_name = await scanner.async_get_device_name(mac) - seen.add(mac) - - try: - extra_attributes = await scanner.async_get_extra_attributes(mac) - except NotImplementedError: - extra_attributes = {} - - kwargs = { - "mac": mac, - "host_name": host_name, - "source_type": SOURCE_TYPE_ROUTER, - "attributes": { - "scanner": scanner.__class__.__name__, - **extra_attributes, - }, - } - - zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) - if zone_home: - kwargs["gps"] = [ - zone_home.attributes[ATTR_LATITUDE], - zone_home.attributes[ATTR_LONGITUDE], - ] - kwargs["gps_accuracy"] = 0 - - hass.async_create_task(async_see_device(**kwargs)) - - async_track_time_interval(hass, async_device_tracker_scan, interval) - hass.async_create_task(async_device_tracker_scan(None)) From 22f63be30e28bc66ac2e9b3982c59c46e983f6dc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 20 Nov 2020 21:48:23 +0100 Subject: [PATCH 160/430] Remove salt integration for webscraping (#43452) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/salt/__init__.py | 1 - .../components/salt/device_tracker.py | 71 ------------------- homeassistant/components/salt/manifest.json | 7 -- requirements_all.txt | 3 - 6 files changed, 84 deletions(-) delete mode 100644 homeassistant/components/salt/__init__.py delete mode 100644 homeassistant/components/salt/device_tracker.py delete mode 100644 homeassistant/components/salt/manifest.json diff --git a/.coveragerc b/.coveragerc index dd72a42aa77..be67b91ea61 100644 --- a/.coveragerc +++ b/.coveragerc @@ -749,7 +749,6 @@ omit = homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py - homeassistant/components/salt/device_tracker.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0cf31095b52..3902f3f5d5f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -376,7 +376,6 @@ homeassistant/components/rpi_power/* @shenxn @swetoast homeassistant/components/ruckus_unleashed/* @gabe565 homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl -homeassistant/components/salt/* @bjornorri homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps diff --git a/homeassistant/components/salt/__init__.py b/homeassistant/components/salt/__init__.py deleted file mode 100644 index 29c371ece52..00000000000 --- a/homeassistant/components/salt/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The salt component.""" diff --git a/homeassistant/components/salt/device_tracker.py b/homeassistant/components/salt/device_tracker.py deleted file mode 100644 index 7c03403622a..00000000000 --- a/homeassistant/components/salt/device_tracker.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Support for Salt Fiber Box routers.""" -import logging - -from saltbox import RouterLoginException, RouterNotReachableException, SaltBox -import voluptuous as vol - -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__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } -) - - -def get_scanner(hass, config): - """Return the Salt device scanner.""" - scanner = SaltDeviceScanner(config[DOMAIN]) - - # Test whether the router is accessible. - data = scanner.get_salt_data() - return scanner if data is not None else None - - -class SaltDeviceScanner(DeviceScanner): - """This class queries a Salt Fiber Box router.""" - - def __init__(self, config): - """Initialize the scanner.""" - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - self.saltbox = SaltBox(f"http://{host}", username, password) - self.online_clients = [] - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return [client["mac"] for client in self.online_clients] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - for client in self.online_clients: - if client["mac"] == device: - return client["name"] - return None - - def get_salt_data(self): - """Retrieve data from Salt router and return parsed result.""" - try: - clients = self.saltbox.get_online_clients() - return clients - except (RouterLoginException, RouterNotReachableException) as error: - _LOGGER.warning(error) - return None - - def _update_info(self): - """Pull the current information from the Salt router.""" - _LOGGER.debug("Loading data from Salt Fiber Box") - data = self.get_salt_data() - self.online_clients = data or [] diff --git a/homeassistant/components/salt/manifest.json b/homeassistant/components/salt/manifest.json deleted file mode 100644 index cad9b6d3661..00000000000 --- a/homeassistant/components/salt/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "salt", - "name": "Salt Fiber Box", - "documentation": "https://www.home-assistant.io/integrations/salt", - "requirements": ["saltbox==0.1.3"], - "codeowners": ["@bjornorri"] -} diff --git a/requirements_all.txt b/requirements_all.txt index 1583a5327eb..e972f69aeb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1974,9 +1974,6 @@ russound_rio==0.1.7 # homeassistant.components.yamaha rxv==0.6.0 -# homeassistant.components.salt -saltbox==0.1.3 - # homeassistant.components.samsungtv samsungctl[websocket]==0.7.1 From b4381c50059c6c5d303a30ffad3b747579c99332 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Nov 2020 14:47:48 -0700 Subject: [PATCH 161/430] Move Notion logger to a package logger (#43450) --- homeassistant/components/notion/__init__.py | 11 ++++------- homeassistant/components/notion/const.py | 3 +++ homeassistant/components/notion/sensor.py | 7 ++----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 296eb34934b..be4a47c6d85 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -1,7 +1,6 @@ """Support for Notion.""" import asyncio from datetime import timedelta -import logging from aionotion import async_get_client from aionotion.errors import InvalidCredentialsError, NotionError @@ -21,9 +20,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DATA_COORDINATOR, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_COORDINATOR, DOMAIN, LOGGER PLATFORMS = ["binary_sensor", "sensor"] @@ -56,10 +53,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session ) except InvalidCredentialsError: - _LOGGER.error("Invalid username and/or password") + LOGGER.error("Invalid username and/or password") return False except NotionError as err: - _LOGGER.error("Config entry failed: %s", err) + LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err async def async_update(): @@ -94,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.entry_id ] = DataUpdateCoordinator( hass, - _LOGGER, + LOGGER, name=entry.data[CONF_USERNAME], update_interval=DEFAULT_SCAN_INTERVAL, update_method=async_update, diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index 6a6da180374..5541cfedc70 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -1,5 +1,8 @@ """Define constants for the Notion integration.""" +import logging + DOMAIN = "notion" +LOGGER = logging.getLogger(__package__) DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 091dcd324dc..99af00c3b1a 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,5 +1,4 @@ """Support for Notion sensors.""" -import logging from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -8,9 +7,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NotionEntity -from .const import DATA_COORDINATOR, DOMAIN, SENSOR_TEMPERATURE - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_COORDINATOR, DOMAIN, LOGGER, SENSOR_TEMPERATURE SENSOR_TYPES = {SENSOR_TEMPERATURE: ("Temperature", "temperature", TEMP_CELSIUS)} @@ -84,7 +81,7 @@ class NotionSensor(NotionEntity): if task["task_type"] == SENSOR_TEMPERATURE: self._state = round(float(task["status"]["value"]), 1) else: - _LOGGER.error( + LOGGER.error( "Unknown task type: %s: %s", self.coordinator.data["sensors"][self._sensor_id], task["task_type"], From cab186d28fa789b88f7dc52544cf0d56d60cb028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 20 Nov 2020 23:59:11 +0200 Subject: [PATCH 162/430] Add ONVIF PTZ Stop support (#39734) --- homeassistant/components/onvif/camera.py | 9 ++++++++- homeassistant/components/onvif/const.py | 1 + homeassistant/components/onvif/device.py | 3 +++ homeassistant/components/onvif/services.yaml | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index d9378588b03..595924fe40d 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -35,6 +35,7 @@ from .const import ( LOGGER, RELATIVE_MOVE, SERVICE_PTZ, + STOP_MOVE, ZOOM_IN, ZOOM_OUT, ) @@ -54,7 +55,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, vol.Optional(ATTR_SPEED, default=0.5): cv.small_float, vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In( - [CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE, GOTOPRESET_MOVE] + [ + CONTINUOUS_MOVE, + RELATIVE_MOVE, + ABSOLUTE_MOVE, + GOTOPRESET_MOVE, + STOP_MOVE, + ] ), vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float, vol.Optional(ATTR_PRESET, default="0"): cv.string, diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py index 2ac78622f05..dc0688c4b30 100644 --- a/homeassistant/components/onvif/const.py +++ b/homeassistant/components/onvif/const.py @@ -39,5 +39,6 @@ CONTINUOUS_MOVE = "ContinuousMove" RELATIVE_MOVE = "RelativeMove" ABSOLUTE_MOVE = "AbsoluteMove" GOTOPRESET_MOVE = "GotoPreset" +STOP_MOVE = "Stop" SERVICE_PTZ = "ptz" diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 599e6084581..84761a4777f 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -28,6 +28,7 @@ from .const import ( LOGGER, PAN_FACTOR, RELATIVE_MOVE, + STOP_MOVE, TILT_FACTOR, ZOOM_FACTOR, ) @@ -433,6 +434,8 @@ class ONVIFDevice: "Zoom": {"x": speed_val}, } await ptz_service.GotoPreset(req) + elif move_mode == STOP_MOVE: + await ptz_service.Stop(req) except ONVIFError as err: if "Bad Request" in err.reason: LOGGER.warning("Device '%s' doesn't support PTZ.", self.name) diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index e5a8c9fce35..bed426e9924 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -29,6 +29,6 @@ ptz: description: "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset" example: "1" move_mode: - description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove or GotoPreset" + description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove, GotoPreset, or Stop" default: "RelativeMove" example: "ContinuousMove" From 43955d3aa83ac01ead51a629e3c9cb20ffc4c878 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 21 Nov 2020 00:08:32 +0000 Subject: [PATCH 163/430] [ci skip] Translation update --- .../components/arcam_fmj/translations/it.json | 4 --- .../components/aurora/translations/ka.json | 26 +++++++++++++++++ .../binary_sensor/translations/ca.json | 2 +- .../binary_sensor/translations/ka.json | 20 +++++++++++++ .../components/cloud/translations/ka.json | 16 +++++++++++ .../components/demo/translations/it.json | 6 ---- .../device_tracker/translations/ka.json | 8 ++++++ .../components/dexcom/translations/ka.json | 7 +++++ .../dialogflow/translations/ka.json | 7 +++++ .../components/directv/translations/it.json | 4 --- .../components/dsmr/translations/it.json | 8 ------ .../components/dsmr/translations/ka.json | 12 ++++++++ .../components/epson/translations/ka.json | 16 +++++++++++ .../components/firmata/translations/it.json | 4 --- .../components/geofency/translations/ka.json | 7 +++++ .../components/gpslogger/translations/ka.json | 7 +++++ .../components/hassio/translations/ka.json | 18 ++++++++++++ .../homeassistant/translations/ka.json | 20 +++++++++++++ .../components/ifttt/translations/ka.json | 7 +++++ .../components/konnected/translations/it.json | 4 +-- .../components/life360/translations/ka.json | 11 ++++++++ .../components/local_ip/translations/cs.json | 1 + .../components/local_ip/translations/en.json | 1 + .../components/local_ip/translations/ka.json | 9 ++++++ .../components/local_ip/translations/no.json | 1 + .../components/locative/translations/ka.json | 7 +++++ .../components/lovelace/translations/ka.json | 10 +++++++ .../components/mailgun/translations/ka.json | 7 +++++ .../components/ozw/translations/ka.json | 28 +++++++++++++++++++ .../components/plaato/translations/ka.json | 7 +++++ .../components/plugwise/translations/ka.json | 11 ++++++++ .../rainmachine/translations/ka.json | 9 ++++++ .../recollect_waste/translations/ka.json | 18 ++++++++++++ .../components/rfxtrx/translations/it.json | 10 ++----- .../components/roku/translations/it.json | 4 --- .../srp_energy/translations/ca.json | 24 ++++++++++++++++ .../srp_energy/translations/cs.json | 23 +++++++++++++++ .../srp_energy/translations/en.json | 6 ++-- .../srp_energy/translations/et.json | 24 ++++++++++++++++ .../srp_energy/translations/ka.json | 23 +++++++++++++++ .../srp_energy/translations/no.json | 23 +++++++++++++++ .../synology_dsm/translations/ka.json | 16 +++++++++++ .../components/traccar/translations/ka.json | 7 +++++ .../components/tuya/translations/ka.json | 23 +++++++++++++++ .../components/twilio/translations/ka.json | 7 +++++ .../components/twinkly/translations/ca.json | 19 +++++++++++++ .../components/twinkly/translations/cs.json | 1 + .../components/twinkly/translations/es.json | 19 +++++++++++++ .../components/twinkly/translations/it.json | 19 +++++++++++++ .../components/twinkly/translations/ka.json | 19 +++++++++++++ .../components/twinkly/translations/no.json | 19 +++++++++++++ .../components/twinkly/translations/pl.json | 19 +++++++++++++ .../components/unifi/translations/it.json | 6 ---- .../components/unifi/translations/ka.json | 11 ++++++++ .../components/upnp/translations/it.json | 4 --- .../components/vizio/translations/ka.json | 7 +++++ 56 files changed, 601 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/aurora/translations/ka.json create mode 100644 homeassistant/components/binary_sensor/translations/ka.json create mode 100644 homeassistant/components/cloud/translations/ka.json create mode 100644 homeassistant/components/device_tracker/translations/ka.json create mode 100644 homeassistant/components/dexcom/translations/ka.json create mode 100644 homeassistant/components/dialogflow/translations/ka.json create mode 100644 homeassistant/components/dsmr/translations/ka.json create mode 100644 homeassistant/components/epson/translations/ka.json create mode 100644 homeassistant/components/geofency/translations/ka.json create mode 100644 homeassistant/components/gpslogger/translations/ka.json create mode 100644 homeassistant/components/hassio/translations/ka.json create mode 100644 homeassistant/components/homeassistant/translations/ka.json create mode 100644 homeassistant/components/ifttt/translations/ka.json create mode 100644 homeassistant/components/life360/translations/ka.json create mode 100644 homeassistant/components/local_ip/translations/ka.json create mode 100644 homeassistant/components/locative/translations/ka.json create mode 100644 homeassistant/components/lovelace/translations/ka.json create mode 100644 homeassistant/components/mailgun/translations/ka.json create mode 100644 homeassistant/components/ozw/translations/ka.json create mode 100644 homeassistant/components/plaato/translations/ka.json create mode 100644 homeassistant/components/plugwise/translations/ka.json create mode 100644 homeassistant/components/rainmachine/translations/ka.json create mode 100644 homeassistant/components/recollect_waste/translations/ka.json create mode 100644 homeassistant/components/srp_energy/translations/ca.json create mode 100644 homeassistant/components/srp_energy/translations/cs.json create mode 100644 homeassistant/components/srp_energy/translations/et.json create mode 100644 homeassistant/components/srp_energy/translations/ka.json create mode 100644 homeassistant/components/srp_energy/translations/no.json create mode 100644 homeassistant/components/synology_dsm/translations/ka.json create mode 100644 homeassistant/components/traccar/translations/ka.json create mode 100644 homeassistant/components/tuya/translations/ka.json create mode 100644 homeassistant/components/twilio/translations/ka.json create mode 100644 homeassistant/components/twinkly/translations/ca.json create mode 100644 homeassistant/components/twinkly/translations/es.json create mode 100644 homeassistant/components/twinkly/translations/it.json create mode 100644 homeassistant/components/twinkly/translations/ka.json create mode 100644 homeassistant/components/twinkly/translations/no.json create mode 100644 homeassistant/components/twinkly/translations/pl.json create mode 100644 homeassistant/components/unifi/translations/ka.json create mode 100644 homeassistant/components/vizio/translations/ka.json diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json index 649bc0f2b39..f5cef4cd8b0 100644 --- a/homeassistant/components/arcam_fmj/translations/it.json +++ b/homeassistant/components/arcam_fmj/translations/it.json @@ -5,10 +5,6 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi" }, - "error": { - "one": "uno", - "other": "altri" - }, "flow_title": "Arcam FMJ su {host}", "step": { "confirm": { diff --git a/homeassistant/components/aurora/translations/ka.json b/homeassistant/components/aurora/translations/ka.json new file mode 100644 index 00000000000..f677f54e32e --- /dev/null +++ b/homeassistant/components/aurora/translations/ka.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0" + }, + "step": { + "user": { + "data": { + "latitude": "\u10d2\u10d0\u10dc\u10d4\u10d3\u10d8", + "longitude": "\u10d2\u10e0\u10eb\u10d4\u10d3\u10d8", + "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u10d6\u10e6\u10d5\u10d0\u10e0\u10d8 (%)" + } + } + } + }, + "title": "NOAA Aurora \u10e1\u10d4\u10dc\u10e1\u10dd\u10e0\u10d8" +} \ 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 8eaa35284c2..9c92a50246a 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -127,7 +127,7 @@ "on": "Calent" }, "light": { - "off": "Sense llum", + "off": "No s'ha detectat llum", "on": "Llum detectada" }, "lock": { diff --git a/homeassistant/components/binary_sensor/translations/ka.json b/homeassistant/components/binary_sensor/translations/ka.json new file mode 100644 index 00000000000..25f9ea4e702 --- /dev/null +++ b/homeassistant/components/binary_sensor/translations/ka.json @@ -0,0 +1,20 @@ +{ + "state": { + "battery_charging": { + "off": "\u10d0\u10e0 \u10d8\u10e2\u10d4\u10dc\u10d4\u10d1\u10d0", + "on": "\u10d8\u10e2\u10d4\u10dc\u10d4\u10d1\u10d0" + }, + "light": { + "off": "\u10e1\u10d8\u10dc\u10d0\u10d7\u10da\u10d4 \u1c90\u10e0 \u10d0\u10e0\u10d8\u10e1", + "on": "\u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10da\u10d8\u10d0 \u10e1\u10d8\u10dc\u10d0\u10d7\u10da\u10d4" + }, + "moving": { + "off": "\u10d0\u10e0 \u10db\u10dd\u10eb\u10e0\u10d0\u10dd\u10d1\u10e1", + "on": "\u10db\u10dd\u10eb\u10e0\u10d0\u10dd\u10d1\u10d0" + }, + "plug": { + "off": "\u10d2\u10d0\u10db\u10dd\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8", + "on": "\u1ca8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/ka.json b/homeassistant/components/cloud/translations/ka.json new file mode 100644 index 00000000000..44507245daa --- /dev/null +++ b/homeassistant/components/cloud/translations/ka.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "can_reach_cert_server": "\u10db\u10d8\u10d4\u10e6\u10ec\u10d0 \u10e1\u10d4\u10e0\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d8\u10e1 \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10e1", + "can_reach_cloud": "\u10db\u10d8\u10d4\u10e6\u10ec\u10d0 Home Assistant \u10e6\u10e0\u10e3\u10d1\u10d4\u10da\u10e1", + "can_reach_cloud_auth": "\u10db\u10d8\u10d4\u10e6\u10ec\u10d0 \u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10e1", + "google_enabled": "Google \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "logged_in": "\u10e8\u10d4\u10e1\u10e3\u10da\u10d8", + "relayer_connected": "\u10e0\u10d4\u10da\u10d4 \u10db\u10d8\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "remote_connected": "\u10d3\u10d8\u10e1\u10e2\u10d0\u10dc\u10ea\u10d8\u10e3\u10e0\u10d0\u10d3 \u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "remote_enabled": "\u10d3\u10d8\u10e1\u10e2\u10d0\u10dc\u10ea\u10d8\u10e3\u10e0\u10d8 \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "subscription_expiration": "\u10d2\u10d0\u10db\u10dd\u10ec\u10d4\u10e0\u10d8\u10e1 \u10d5\u10d0\u10d3\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d5\u10da\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 50939df5631..dc3e218895b 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,12 +1,6 @@ { "options": { "step": { - "init": { - "data": { - "one": "uno", - "other": "altri" - } - }, "options_1": { "data": { "bool": "Valore booleano facoltativo", diff --git a/homeassistant/components/device_tracker/translations/ka.json b/homeassistant/components/device_tracker/translations/ka.json new file mode 100644 index 00000000000..8d01dff16e7 --- /dev/null +++ b/homeassistant/components/device_tracker/translations/ka.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "enters": "{entity_name} \u10e8\u10d4\u10d3\u10d8\u10e1 \u10d6\u10dd\u10dc\u10d0\u10e8\u10d8", + "leaves": "{entity_name} \u10e2\u10dd\u10d5\u10d4\u10d1\u10e1 \u10d6\u10dd\u10dc\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/ka.json b/homeassistant/components/dexcom/translations/ka.json new file mode 100644 index 00000000000..834e00eaa97 --- /dev/null +++ b/homeassistant/components/dexcom/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/ka.json b/homeassistant/components/dialogflow/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/dialogflow/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json index cdb2f8b418d..2b6e25d63ef 100644 --- a/homeassistant/components/directv/translations/it.json +++ b/homeassistant/components/directv/translations/it.json @@ -10,10 +10,6 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "data": { - "one": "uno", - "other": "altri" - }, "description": "Vuoi impostare {name} ?" }, "user": { diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index 9330c89a42a..75cbb713056 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -2,14 +2,6 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" - }, - "error": { - "one": "uno", - "other": "altri" - }, - "step": { - "one": "uno", - "other": "altri" } }, "options": { diff --git a/homeassistant/components/dsmr/translations/ka.json b/homeassistant/components/dsmr/translations/ka.json new file mode 100644 index 00000000000..df4b7b039f9 --- /dev/null +++ b/homeassistant/components/dsmr/translations/ka.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d4\u10d1\u10e1 \u10e8\u10dd\u10e0\u10d8\u10e1 \u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10e0\u10dd" + }, + "title": "DSMR \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10e0\u10d4\u10d1\u10d8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/ka.json b/homeassistant/components/epson/translations/ka.json new file mode 100644 index 00000000000..b339899ea5f --- /dev/null +++ b/homeassistant/components/epson/translations/ka.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + }, + "step": { + "user": { + "data": { + "host": "\u10f0\u10dd\u10e1\u10e2\u10d8", + "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8", + "port": "\u10de\u10dd\u10e0\u10e2\u10d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/it.json b/homeassistant/components/firmata/translations/it.json index 6c2460ab0b2..a4f6f9e7222 100644 --- a/homeassistant/components/firmata/translations/it.json +++ b/homeassistant/components/firmata/translations/it.json @@ -2,10 +2,6 @@ "config": { "abort": { "cannot_connect": "Impossibile connettersi" - }, - "step": { - "one": "uno", - "other": "altri" } } } \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ka.json b/homeassistant/components/geofency/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/geofency/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/ka.json b/homeassistant/components/gpslogger/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/gpslogger/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ka.json b/homeassistant/components/hassio/translations/ka.json new file mode 100644 index 00000000000..7a224350df7 --- /dev/null +++ b/homeassistant/components/hassio/translations/ka.json @@ -0,0 +1,18 @@ +{ + "system_health": { + "info": { + "board": "\u10d3\u10d0\u10e4\u10d0", + "disk_total": "\u10d3\u10d8\u10e1\u10d9\u10d6\u10d4 \u10e1\u10e3\u10da", + "disk_used": "\u10d3\u10d8\u10e1\u10d9\u10d6\u10d4 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8", + "docker_version": "Docker-\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", + "healthy": "\u10d2\u10d0\u10db\u10d0\u10e0\u10d7\u10e3\u10da\u10d8\u10d0", + "host_os": "\u10f0\u10dd\u10e1\u10e2 \u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d0", + "installed_addons": "\u10d3\u10d0\u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8 \u10d3\u10d0\u10db\u10d0\u10e2\u10d4\u10d1\u10d4\u10d1\u10d8", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", + "supported": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d0", + "update_channel": "\u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10d0\u10e0\u10ee\u10d8", + "version_api": "API-\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ka.json b/homeassistant/components/homeassistant/translations/ka.json new file mode 100644 index 00000000000..0acadcdf3a1 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/ka.json @@ -0,0 +1,20 @@ +{ + "system_health": { + "info": { + "arch": "\u10de\u10e0\u10dd\u10ea\u10d4\u10e1\u10dd\u10e0\u10d8\u10e1 \u10d0\u10e0\u10e5\u10d8\u10e2\u10d4\u10e5\u10e2\u10e3\u10e0\u10d0", + "chassis": "\u10e8\u10d0\u10e1\u10d8", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant \u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8", + "installation_type": "\u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8", + "os_name": "\u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d8\u10e1 \u10dd\u10ef\u10d0\u10ee\u10d8", + "os_version": "\u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", + "python_version": "Python-\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", + "supervisor": "Supervisor", + "timezone": "\u1c93\u10e0\u10dd\u10d8\u10e1 \u10e1\u10d0\u10e0\u10e2\u10e7\u10d4\u10da\u10d8", + "version": "\u10d5\u10d4\u10e0\u10e1\u10d8\u10d0", + "virtualenv": "\u10d5\u10d8\u10e0\u10e2\u10e3\u10d0\u10da\u10e3\u10e0\u10d8 \u10d2\u10d0\u10e0\u10d4\u10db\u10dd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/ka.json b/homeassistant/components/ifttt/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/ifttt/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 35cc0f1a4c2..b618ee04b48 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -32,9 +32,7 @@ "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto" }, "error": { - "bad_host": "URL dell'host API di sostituzione non valido", - "one": "uno", - "other": "altri" + "bad_host": "URL dell'host API di sostituzione non valido" }, "step": { "options_binary": { diff --git a/homeassistant/components/life360/translations/ka.json b/homeassistant/components/life360/translations/ka.json new file mode 100644 index 00000000000..4069ab18e6f --- /dev/null +++ b/homeassistant/components/life360/translations/ka.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + }, + "error": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/cs.json b/homeassistant/components/local_ip/translations/cs.json index 239d312b3f9..f7254ebaf44 100644 --- a/homeassistant/components/local_ip/translations/cs.json +++ b/homeassistant/components/local_ip/translations/cs.json @@ -8,6 +8,7 @@ "data": { "name": "N\u00e1zev senzoru" }, + "description": "Chcete za\u010d\u00edt nastavovat?", "title": "M\u00edstn\u00ed IP adresa" } } diff --git a/homeassistant/components/local_ip/translations/en.json b/homeassistant/components/local_ip/translations/en.json index 7f823968f9c..167989b7ba8 100644 --- a/homeassistant/components/local_ip/translations/en.json +++ b/homeassistant/components/local_ip/translations/en.json @@ -8,6 +8,7 @@ "data": { "name": "Sensor Name" }, + "description": "Do you want to start set up?", "title": "Local IP Address" } } diff --git a/homeassistant/components/local_ip/translations/ka.json b/homeassistant/components/local_ip/translations/ka.json new file mode 100644 index 00000000000..62dd468b5c1 --- /dev/null +++ b/homeassistant/components/local_ip/translations/ka.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u10d2\u10dc\u10d4\u10d1\u10d0\u10d5\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10d3\u10d0\u10ec\u10e7\u10d4\u10d1\u10d0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/no.json b/homeassistant/components/local_ip/translations/no.json index cf229e06877..f5686ee5236 100644 --- a/homeassistant/components/local_ip/translations/no.json +++ b/homeassistant/components/local_ip/translations/no.json @@ -8,6 +8,7 @@ "data": { "name": "Sensornavn" }, + "description": "Vil du starte oppsettet?", "title": "Lokal IP-adresse" } } diff --git a/homeassistant/components/locative/translations/ka.json b/homeassistant/components/locative/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/locative/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/ka.json b/homeassistant/components/lovelace/translations/ka.json new file mode 100644 index 00000000000..a10a1f66ac8 --- /dev/null +++ b/homeassistant/components/lovelace/translations/ka.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "\u10d3\u10d4\u10e8\u10d1\u10dd\u10e0\u10d3\u10d8", + "mode": "\u10e0\u10d4\u10df\u10d8\u10db\u10d8", + "resources": "\u10e0\u10d4\u10e1\u10e3\u10e0\u10e1\u10d4\u10d1\u10d8", + "views": "\u10ee\u10d4\u10d3\u10d4\u10d1\u10d8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ka.json b/homeassistant/components/mailgun/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/mailgun/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ka.json b/homeassistant/components/ozw/translations/ka.json new file mode 100644 index 00000000000..6121721be50 --- /dev/null +++ b/homeassistant/components/ozw/translations/ka.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "addon_info_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e4\u10dd\u10e1 \u10db\u10d8\u10e6\u10d4\u10d1\u10d0.", + "addon_install_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10d0.", + "addon_set_config_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0." + }, + "error": { + "addon_start_failed": "OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8 \u10d0\u10e0 \u10d3\u10d0\u10d8\u10e1\u10e2\u10d0\u10e0\u10e2\u10d0. \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + }, + "step": { + "on_supervisor": { + "data": { + "use_addon": "\u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d7 OpenZWave Supervisor \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8" + }, + "description": "\u10d2\u10e1\u10e3\u10e0\u10d7 \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10dd\u10d7 OpenZWave Supervisor-\u10d8\u10e1 \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8?", + "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8" + }, + "start_addon": { + "data": { + "network_key": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d0\u10e6\u10d4\u10d1\u10d8", + "usb_path": "USB \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d6\u10d0" + }, + "title": "\u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10d4\u10d7 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ka.json b/homeassistant/components/plaato/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/plaato/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ka.json b/homeassistant/components/plugwise/translations/ka.json new file mode 100644 index 00000000000..f009e98e40b --- /dev/null +++ b/homeassistant/components/plugwise/translations/ka.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user_gateway": { + "data": { + "username": "\u10e6\u10d8\u10db\u10d8\u10da\u10d8\u10d0\u10dc\u10d8 \u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/ka.json b/homeassistant/components/rainmachine/translations/ka.json new file mode 100644 index 00000000000..0c7e50133f4 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/ka.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "title": "RainMachine-\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/ka.json b/homeassistant/components/recollect_waste/translations/ka.json new file mode 100644 index 00000000000..796ec66cf7f --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/ka.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "error": { + "invalid_place_or_service_id": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10d3\u10d2\u10d8\u10da\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10dd\u10d1\u10d8\u10e1 \u10d0\u10dc \u10e1\u10d4\u10e0\u10d5\u10d8\u10e1\u10d8\u10e1 ID" + }, + "step": { + "user": { + "data": { + "place_id": "\u10d0\u10d3\u10d2\u10d8\u10da\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10dd\u10d1\u10d8\u10e1 ID", + "service_id": "\u10e1\u10d4\u10e0\u10d5\u10d8\u10e1\u10d8\u10e1 ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index a97cfdd2ce4..ff705fdd0a2 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -5,13 +5,9 @@ "cannot_connect": "Impossibile connettersi" }, "error": { - "cannot_connect": "Impossibile connettersi", - "one": "uno", - "other": "altri" + "cannot_connect": "Impossibile connettersi" }, "step": { - "one": "uno", - "other": "altri", "setup_network": { "data": { "host": "Host", @@ -39,7 +35,6 @@ } } }, - "one": "uno", "options": { "error": { "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", @@ -74,6 +69,5 @@ "title": "Configurare le opzioni del dispositivo" } } - }, - "other": "altri" + } } \ No newline at end of file diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index abbe29fb2a7..007be91d155 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -10,10 +10,6 @@ "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { - "data": { - "one": "uno", - "other": "altri" - }, "description": "Vuoi impostare {name}?", "title": "Roku" }, diff --git a/homeassistant/components/srp_energy/translations/ca.json b/homeassistant/components/srp_energy/translations/ca.json new file mode 100644 index 00000000000..c6617e617d4 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_account": "L'ID del compte ha de ser un n\u00famero de 9 d\u00edgits", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "id": "ID del compte", + "is_tou": "\u00c9s tarifa de temps d'\u00fas", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/cs.json b/homeassistant/components/srp_energy/translations/cs.json new file mode 100644 index 00000000000..74b4bd53090 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_account": "ID \u00fa\u010dtu by m\u011blo b\u00fdt 9m\u00edstn\u00e9 \u010d\u00edslo", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "id": "ID \u00fa\u010dtu", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/en.json b/homeassistant/components/srp_energy/translations/en.json index 0d00872e43d..99926b18b4f 100644 --- a/homeassistant/components/srp_energy/translations/en.json +++ b/homeassistant/components/srp_energy/translations/en.json @@ -5,17 +5,17 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "invalid_account": "Account ID should be a 9 digit number", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { "user": { "data": { "id": "Account Id", - "username": "Username", + "is_tou": "Is Time of Use Plan", "password": "Password", - "is_tou": "Is Time of Use Plan" + "username": "Username" } } } diff --git a/homeassistant/components/srp_energy/translations/et.json b/homeassistant/components/srp_energy/translations/et.json new file mode 100644 index 00000000000..558bb4a19ed --- /dev/null +++ b/homeassistant/components/srp_energy/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_account": "Konto ID peab olema 9-kohaline number", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "id": "Konto ID", + "is_tou": "Kas kasutusaja plaan", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ka.json b/homeassistant/components/srp_energy/translations/ka.json new file mode 100644 index 00000000000..a281500e91c --- /dev/null +++ b/homeassistant/components/srp_energy/translations/ka.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10da\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0" + }, + "error": { + "cannot_connect": "\u10e8\u10d4\u10d4\u10e0\u10d7\u10d0\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", + "invalid_account": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 ID \u10e3\u10dc\u10d3\u10d0 \u10d8\u10e7\u10dd\u10e1 9 \u10ea\u10d8\u10e4\u10e0\u10d8\u10d0\u10dc\u10d8 \u10dc\u10dd\u10db\u10d4\u10e0\u10d8", + "invalid_auth": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", + "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + }, + "step": { + "user": { + "data": { + "id": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 ID", + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8", + "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/no.json b/homeassistant/components/srp_energy/translations/no.json new file mode 100644 index 00000000000..a3177325024 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_account": "Konto ID skal v\u00e6re et ni-sifret nummer", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "id": "Konto ID", + "password": "Passord", + "username": "Brukernavn" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ka.json b/homeassistant/components/synology_dsm/translations/ka.json new file mode 100644 index 00000000000..507e374be42 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/ka.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "link": { + "data": { + "verify_ssl": "SSL \u10e1\u10d4\u10e0\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d8\u10e1 \u10e8\u10d4\u10db\u10dd\u10ec\u10db\u10d4\u10d1\u10d0" + } + }, + "user": { + "data": { + "verify_ssl": "SSL \u10e1\u10d4\u10e0\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d8\u10e1 \u10e8\u10d4\u10db\u10dd\u10ec\u10db\u10d4\u10d1\u10d0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/ka.json b/homeassistant/components/traccar/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/traccar/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ka.json b/homeassistant/components/tuya/translations/ka.json new file mode 100644 index 00000000000..4418dc65d35 --- /dev/null +++ b/homeassistant/components/tuya/translations/ka.json @@ -0,0 +1,23 @@ +{ + "options": { + "step": { + "device": { + "data": { + "unit_of_measurement": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10d4\u10e0\u10d7\u10d4\u10e3\u10da\u10d8" + }, + "description": "\u10d3\u10d0\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d3 {device_type} `{device_name}` \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10d4\u10e0\u10d1\u10d8 \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e9\u10d5\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10db\u10dd\u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10d0\u10d3", + "title": "Tuya-\u10e1 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" + }, + "init": { + "data": { + "discovery_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8", + "list_devices": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d0\u10dc \u10d3\u10d0\u10e2\u10dd\u10d5\u10d4\u10d7 \u10ea\u10d0\u10e0\u10d8\u10d4\u10da\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e8\u10d4\u10e1\u10d0\u10dc\u10d0\u10ee\u10d0\u10d3", + "query_device": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0, \u10e0\u10dd\u10db\u10d4\u10da\u10d8\u10ea \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d1\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10e1 \u10e1\u10e2\u10d0\u10e2\u10e3\u10e1\u10d8\u10e1 \u10e1\u10ec\u10e0\u10d0\u10e4\u10d8 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1", + "query_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8" + }, + "description": "\u10d0\u10e0 \u10d3\u10d0\u10d0\u10e7\u10d4\u10dc\u10dd\u10d7 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10dd\u10d1\u10d4\u10d1\u10d8 \u10eb\u10d0\u10da\u10d8\u10d0\u10dc \u10db\u10ea\u10d8\u10e0\u10d4 \u10d7\u10dd\u10e0\u10d4\u10d1 \u10d2\u10d0\u10db\u10dd\u10eb\u10d0\u10ee\u10d4\u10d1\u10d4\u10d1\u10d8 \u10d3\u10d0\u10d0\u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d4\u10dc \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d4\u10d1\u10e1 \u10da\u10dd\u10d2\u10e8\u10d8", + "title": "Tuya-\u10e1 \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10e0\u10d4\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/ka.json b/homeassistant/components/twilio/translations/ka.json new file mode 100644 index 00000000000..a284a55fbcf --- /dev/null +++ b/homeassistant/components/twilio/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/ca.json b/homeassistant/components/twinkly/translations/ca.json new file mode 100644 index 00000000000..2801361990e --- /dev/null +++ b/homeassistant/components/twinkly/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 (o adre\u00e7a IP) del dispositiu Twinkly" + }, + "description": "Configura la teva tira LED de Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/cs.json b/homeassistant/components/twinkly/translations/cs.json index e3b749bfee3..edb776e1483 100644 --- a/homeassistant/components/twinkly/translations/cs.json +++ b/homeassistant/components/twinkly/translations/cs.json @@ -11,6 +11,7 @@ "data": { "host": "Hostitel (nebo IP adresa) va\u0161eho za\u0159\u00edzen\u00ed Twinkly" }, + "description": "Nastavte sv\u016fj LED p\u00e1sek Twinkly", "title": "Twinkly" } } diff --git a/homeassistant/components/twinkly/translations/es.json b/homeassistant/components/twinkly/translations/es.json new file mode 100644 index 00000000000..60024dc2d1e --- /dev/null +++ b/homeassistant/components/twinkly/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host (o direcci\u00f3n IP) de tu dispositivo Twinkly" + }, + "description": "Configura tu tira led Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/it.json b/homeassistant/components/twinkly/translations/it.json new file mode 100644 index 00000000000..ec62abeea01 --- /dev/null +++ b/homeassistant/components/twinkly/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host (o indirizzo IP) del tuo dispositivo twinkly" + }, + "description": "Configura la tua stringa led Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/ka.json b/homeassistant/components/twinkly/translations/ka.json new file mode 100644 index 00000000000..d0d6b61f4cc --- /dev/null +++ b/homeassistant/components/twinkly/translations/ka.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "error": { + "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0" + }, + "step": { + "user": { + "data": { + "host": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 twinkly \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10f0\u10dd\u10e1\u10e2\u10d8 (\u10d0\u10dc IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8)" + }, + "description": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Twinkly \u10e8\u10e3\u10e5\u10d3\u10d8\u10dd\u10d3\u10d8\u10e1 \u10da\u10d4\u10dc\u10e2\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/no.json b/homeassistant/components/twinkly/translations/no.json new file mode 100644 index 00000000000..2bfe2d3606a --- /dev/null +++ b/homeassistant/components/twinkly/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert (eller IP-adresse) for din twinkly-enhet" + }, + "description": "Sett opp Twinkly-led-strengen", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/pl.json b/homeassistant/components/twinkly/translations/pl.json new file mode 100644 index 00000000000..2c1434dc035 --- /dev/null +++ b/homeassistant/components/twinkly/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Konfiguracja Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index e530f600a04..79a7206923e 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -45,12 +45,6 @@ "description": "Configurare il tracciamento del dispositivo", "title": "Opzioni UniFi 1/3" }, - "init": { - "data": { - "one": "uno", - "other": "altri" - } - }, "simple_options": { "data": { "block_client": "Client controllati per l'accesso alla rete", diff --git a/homeassistant/components/unifi/translations/ka.json b/homeassistant/components/unifi/translations/ka.json new file mode 100644 index 00000000000..31cbabe0d97 --- /dev/null +++ b/homeassistant/components/unifi/translations/ka.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "client_control": { + "data": { + "dpi_restrictions": "DPI \u10e8\u10d4\u10d6\u10e6\u10e3\u10d3\u10d5\u10d8\u10e1 \u10ef\u10d2\u10e3\u10e4\u10d4\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e2\u10e0\u10dd\u10da\u10d8\u10e1 \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10d0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index 4955ffaa52e..ca4e376432c 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -11,10 +11,6 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "init": { - "one": "uno", - "other": "altri" - }, "ssdp_confirm": { "description": "Vuoi configurare questo dispositivo UPnP/IGD?" }, diff --git a/homeassistant/components/vizio/translations/ka.json b/homeassistant/components/vizio/translations/ka.json new file mode 100644 index 00000000000..e394fa786b0 --- /dev/null +++ b/homeassistant/components/vizio/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0" + } + } +} \ No newline at end of file From e32669a2d9472487033ec161bc1bba1fc17ade2a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 20 Nov 2020 20:02:52 -0500 Subject: [PATCH 164/430] Remove zigpy monkey patching (#43456) * Use event handler for incoming messages from zigpy * Bump up zha dependency --- .coveragerc | 1 - homeassistant/components/zha/core/gateway.py | 22 ++++++++++++-------- homeassistant/components/zha/core/patches.py | 18 ---------------- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 16 insertions(+), 31 deletions(-) delete mode 100644 homeassistant/components/zha/core/patches.py diff --git a/.coveragerc b/.coveragerc index be67b91ea61..7186718fecd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1057,7 +1057,6 @@ 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/core/typing.py homeassistant/components/zha/entity.py diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index bdfcf1b24f2..69bf85e47a4 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -82,7 +82,6 @@ from .device import ( ZHADevice, ) from .group import GroupMember, ZHAGroup -from .patches import apply_application_controller_patch from .registries import GROUP_ENTITY_DOMAINS from .store import async_get_registry from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType @@ -155,7 +154,6 @@ class ZHAGateway: ) raise ConfigEntryNotReady from exception - apply_application_controller_patch(self) self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self @@ -506,13 +504,6 @@ class ZHAGateway: self._groups[zigpy_group.group_id] = zha_group return zha_group - @callback - def async_device_became_available( - self, sender, profile, cluster, src_ep, dst_ep, message - ): - """Handle tasks when a device becomes available.""" - self.async_update_device(sender, available=True) - @callback def async_update_device( self, sender: zigpy_dev.Device, available: bool = True @@ -639,6 +630,19 @@ class ZHAGateway: unsubscribe() await self.application_controller.pre_shutdown() + def handle_message( + self, + sender: zigpy_dev.Device, + profile: int, + cluster: int, + src_ep: int, + dst_ep: int, + message: bytes, + ) -> None: + """Handle message from a device Event handler.""" + if sender.ieee in self.devices and not self.devices[sender.ieee].available: + self.async_update_device(sender, available=True) + @callback def async_capture_log_levels(): diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py deleted file mode 100644 index 633152e253c..00000000000 --- a/homeassistant/components/zha/core/patches.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Patch functions for Zigbee Home Automation.""" - - -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, profile, cluster, src_ep, dst_ep, message): - """Handle message from a device.""" - if ( - sender.ieee in zha_gateway.devices - and not zha_gateway.devices[sender.ieee].available - ): - zha_gateway.async_device_became_available( - sender, profile, cluster, src_ep, dst_ep, message - ) - 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 176338e02c8..34fd5f1f461 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zha-quirks==0.0.46", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.27.1", + "zigpy==0.28.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.2.2" diff --git a/requirements_all.txt b/requirements_all.txt index e972f69aeb8..e4672196675 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2362,7 +2362,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.2.2 # homeassistant.components.zha -zigpy==0.27.1 +zigpy==0.28.0 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2025493aaa..6c37691fd2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1146,4 +1146,4 @@ zigpy-zigate==0.7.3 zigpy-znp==0.2.2 # homeassistant.components.zha -zigpy==0.27.1 +zigpy==0.28.0 From db60a7160323cad6a1c761db1f3cea5d8388891b Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 21 Nov 2020 03:43:20 +0100 Subject: [PATCH 165/430] Change Plugwise integration to plugwise module (#43036) * Switch to plugwise module and forthcoming changes * Adjusted according to review * Fix leaving out domain for tests * Add tests for exceptions * Add more tests for exceptions * Version bump * Wording on test * Catch-up with dev --- CODEOWNERS | 2 +- homeassistant/components/plugwise/__init__.py | 29 +++--------- homeassistant/components/plugwise/climate.py | 10 ++-- .../components/plugwise/config_flow.py | 7 +-- homeassistant/components/plugwise/const.py | 4 +- homeassistant/components/plugwise/gateway.py | 24 ++++++---- .../components/plugwise/manifest.json | 4 +- homeassistant/components/plugwise/switch.py | 16 +++++-- requirements_all.txt | 6 +-- requirements_test_all.txt | 6 +-- tests/components/plugwise/common.py | 2 +- tests/components/plugwise/conftest.py | 41 +++++++++------- tests/components/plugwise/test_climate.py | 47 +++++++++++++++++-- tests/components/plugwise/test_config_flow.py | 18 ++++--- tests/components/plugwise/test_init.py | 6 +-- tests/components/plugwise/test_switch.py | 27 +++++++++++ 16 files changed, 163 insertions(+), 86 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3902f3f5d5f..29740431c37 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -336,7 +336,7 @@ homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plex/* @jjlawren -homeassistant/components/plugwise/* @CoMPaTech @bouwew +homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa homeassistant/components/point/* @fredrike homeassistant/components/poolsense/* @haemishkyd diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index ee6c44bc558..47a9a1e7d9c 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -1,16 +1,10 @@ """Plugwise platform for Home Assistant Core.""" -import asyncio - -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from .const import ALL_PLATFORMS, DOMAIN, UNDO_UPDATE_LISTENER -from .gateway import async_setup_entry_gw - -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +from .gateway import async_setup_entry_gw, async_unload_entry_gw async def async_setup(hass: HomeAssistant, config: dict): @@ -27,19 +21,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 ALL_PLATFORMS - ] - ) - ) - - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + """Unload the Plugwise components.""" + if entry.data.get(CONF_HOST): + return await async_unload_entry_gw(hass, entry) + # PLACEHOLDER USB entry setup + return False diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 7981283f27d..c8a2191963e 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -2,7 +2,7 @@ import logging -from Plugwise_Smile.Smile import Smile +from plugwise.exceptions import PlugwiseException from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -192,7 +192,7 @@ class PwThermostat(SmileGateway, ClimateEntity): await self._api.set_temperature(self._loc_id, temperature) self._setpoint = temperature self.async_write_ha_state() - except Smile.PlugwiseError: + except PlugwiseException: _LOGGER.error("Error while communicating to device") else: _LOGGER.error("Invalid temperature requested") @@ -205,7 +205,7 @@ class PwThermostat(SmileGateway, ClimateEntity): try: await self._api.set_temperature(self._loc_id, self._schedule_temp) self._setpoint = self._schedule_temp - except Smile.PlugwiseError: + except PlugwiseException: _LOGGER.error("Error while communicating to device") try: await self._api.set_schedule_state( @@ -213,7 +213,7 @@ class PwThermostat(SmileGateway, ClimateEntity): ) self._hvac_mode = hvac_mode self.async_write_ha_state() - except Smile.PlugwiseError: + except PlugwiseException: _LOGGER.error("Error while communicating to device") async def async_set_preset_mode(self, preset_mode): @@ -223,7 +223,7 @@ class PwThermostat(SmileGateway, ClimateEntity): self._preset_mode = preset_mode self._setpoint = self._presets.get(self._preset_mode, "none")[0] self.async_write_ha_state() - except Smile.PlugwiseError: + except PlugwiseException: _LOGGER.error("Error while communicating to device") @callback diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 6fd7cde44bc..e0d22627737 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -1,7 +1,8 @@ """Config flow for Plugwise integration.""" import logging -from Plugwise_Smile.Smile import Smile +from plugwise.exceptions import InvalidAuthentication, PlugwiseException +from plugwise.smile import Smile import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -67,9 +68,9 @@ async def validate_gw_input(hass: core.HomeAssistant, data): try: await api.connect() - except Smile.InvalidAuthentication as err: + except InvalidAuthentication as err: raise InvalidAuth from err - except Smile.PlugwiseError as err: + except PlugwiseException as err: raise CannotConnect from err return api diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index f965676aef2..c6ef43af602 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -2,7 +2,9 @@ DOMAIN = "plugwise" SENSOR_PLATFORMS = ["sensor", "switch"] -ALL_PLATFORMS = ["binary_sensor", "climate", "sensor", "switch"] +PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"] +PW_TYPE = "plugwise_type" +GATEWAY = "gateway" # Sensor mapping SENSOR_MAP_DEVICE_CLASS = 2 diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 5ba6eda2770..3b61bd3930d 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -5,8 +5,13 @@ from datetime import timedelta import logging from typing import Dict -from Plugwise_Smile.Smile import Smile import async_timeout +from plugwise.exceptions import ( + InvalidAuthentication, + PlugwiseException, + XMLDataMissingError, +) +from plugwise.smile import Smile import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -28,13 +33,15 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - ALL_PLATFORMS, COORDINATOR, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_TIMEOUT, DEFAULT_USERNAME, DOMAIN, + GATEWAY, + PLATFORMS_GATEWAY, + PW_TYPE, SENSOR_PLATFORMS, UNDO_UPDATE_LISTENER, ) @@ -64,11 +71,11 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unable to connect to Smile") raise ConfigEntryNotReady - except Smile.InvalidAuthentication: + except InvalidAuthentication: _LOGGER.error("Invalid username or Smile ID") return False - except Smile.PlugwiseError as err: + except PlugwiseException as err: _LOGGER.error("Error while communicating to device %s", api.smile_name) raise ConfigEntryNotReady from err @@ -88,7 +95,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(DEFAULT_TIMEOUT): await api.full_update_device() return True - except Smile.XMLDataMissingError as err: + except XMLDataMissingError as err: raise UpdateFailed("Smile update failed") from err coordinator = DataUpdateCoordinator( @@ -115,6 +122,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { "api": api, COORDINATOR: coordinator, + PW_TYPE: GATEWAY, UNDO_UPDATE_LISTENER: undo_listener, } @@ -130,7 +138,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: single_master_thermostat = api.single_master_thermostat() - platforms = ALL_PLATFORMS + platforms = PLATFORMS_GATEWAY if single_master_thermostat is None: platforms = SENSOR_PLATFORMS @@ -150,13 +158,13 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): ) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry_gw(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 ALL_PLATFORMS + for component in PLATFORMS_GATEWAY ] ) ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f431ce9ee97..5a32341139c 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,8 +2,8 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["Plugwise_Smile==1.6.0"], - "codeowners": ["@CoMPaTech", "@bouwew"], + "requirements": ["plugwise==0.8.3"], + "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true } diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 8221bc2cb57..ce3be04681a 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -2,7 +2,7 @@ import logging -from Plugwise_Smile.Smile import Smile +from plugwise.exceptions import PlugwiseException from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback @@ -14,6 +14,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Smile switches from a config entry.""" + # PLACEHOLDER USB entry setup + return await async_setup_entry_gateway(hass, config_entry, async_add_entities) + + +async def async_setup_entry_gateway(hass, config_entry, async_add_entities): """Set up the Smile switches from a config entry.""" api = hass.data[DOMAIN][config_entry.entry_id]["api"] coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] @@ -37,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = "Switch Group" entities.append( - PwSwitch( + GwSwitch( api, coordinator, device_properties["name"], dev_id, members, model ) ) @@ -45,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class PwSwitch(SmileGateway, SwitchEntity): +class GwSwitch(SmileGateway, SwitchEntity): """Representation of a Plugwise plug.""" def __init__(self, api, coordinator, name, dev_id, members, model): @@ -79,7 +85,7 @@ class PwSwitch(SmileGateway, SwitchEntity): if state_on: self._is_on = True self.async_write_ha_state() - except Smile.PlugwiseError: + except PlugwiseException: _LOGGER.error("Error while communicating to device") async def async_turn_off(self, **kwargs): @@ -91,7 +97,7 @@ class PwSwitch(SmileGateway, SwitchEntity): if state_off: self._is_on = False self.async_write_ha_state() - except Smile.PlugwiseError: + except PlugwiseException: _LOGGER.error("Error while communicating to device") @callback diff --git a/requirements_all.txt b/requirements_all.txt index e4672196675..f687f042b1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -25,9 +25,6 @@ Mastodon.py==1.5.1 # homeassistant.components.orangepi_gpio OPi.GPIO==0.4.0 -# homeassistant.components.plugwise -Plugwise_Smile==1.6.0 - # homeassistant.components.essent PyEssent==0.14 @@ -1139,6 +1136,9 @@ plexauth==0.0.6 # homeassistant.components.plex plexwebsocket==0.0.12 +# homeassistant.components.plugwise +plugwise==0.8.3 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c37691fd2a..3d1ad87e1de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -6,9 +6,6 @@ # homeassistant.components.homekit HAP-python==3.0.0 -# homeassistant.components.plugwise -Plugwise_Smile==1.6.0 - # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -562,6 +559,9 @@ plexauth==0.0.6 # homeassistant.components.plex plexwebsocket==0.0.12 +# homeassistant.components.plugwise +plugwise==0.8.3 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/common.py b/tests/components/plugwise/common.py index eb227322aa8..379929ce2f1 100644 --- a/tests/components/plugwise/common.py +++ b/tests/components/plugwise/common.py @@ -1,6 +1,6 @@ """Common initialisation for the Plugwise integration.""" -from homeassistant.components.plugwise import DOMAIN +from homeassistant.components.plugwise.const import DOMAIN from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 938e61146e5..ae934c565bc 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -3,8 +3,13 @@ from functools import partial import re -from Plugwise_Smile.Smile import Smile import jsonpickle +from plugwise.exceptions import ( + ConnectionFailedError, + InvalidAuthentication, + PlugwiseException, + XMLDataMissingError, +) import pytest from tests.async_mock import AsyncMock, Mock, patch @@ -24,8 +29,8 @@ def mock_smile(): with patch( "homeassistant.components.plugwise.config_flow.Smile", ) as smile_mock: - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError smile_mock.return_value.connect.return_value = True yield smile_mock.return_value @@ -48,9 +53,9 @@ def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None: def mock_smile_notconnect(): """Mock the Plugwise Smile general connection failure for Home Assistant.""" with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError - smile_mock.PlugwiseError = Smile.PlugwiseError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.PlugwiseException = PlugwiseException smile_mock.return_value.connect.side_effect = AsyncMock(return_value=False) yield smile_mock.return_value @@ -65,9 +70,9 @@ def mock_smile_adam(): """Create a Mock Adam environment for testing exceptions.""" chosen_env = "adam_multiple_devices_per_zone" with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError - smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.XMLDataMissingError = XMLDataMissingError smile_mock.return_value.gateway_id = "fe799307f1624099878210aa0b9f1475" smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730" @@ -110,9 +115,9 @@ def mock_smile_anna(): """Create a Mock Anna environment for testing exceptions.""" chosen_env = "anna_heatpump" with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError - smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.XMLDataMissingError = XMLDataMissingError smile_mock.return_value.gateway_id = "015ae9ea3f964e668e490fa39da3870b" smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" @@ -155,9 +160,9 @@ def mock_smile_p1(): """Create a Mock P1 DSMR environment for testing exceptions.""" chosen_env = "p1v3_full_option" with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError - smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.XMLDataMissingError = XMLDataMissingError smile_mock.return_value.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3" smile_mock.return_value.heater_id = None @@ -191,9 +196,9 @@ def mock_stretch(): """Create a Mock Stretch environment for testing exceptions.""" chosen_env = "stretch_v31" with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError - smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.XMLDataMissingError = XMLDataMissingError smile_mock.return_value.gateway_id = "259882df3c05415b99c2d962534ce820" smile_mock.return_value.heater_id = None diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 7c74d970d7e..e85140660fd 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -1,5 +1,8 @@ """Tests for the Plugwise Climate integration.""" +from plugwise.exceptions import PlugwiseException + +from homeassistant.components.climate.const import HVAC_MODE_AUTO, HVAC_MODE_HEAT from homeassistant.config_entries import ENTRY_STATE_LOADED from tests.components.plugwise.common import async_init_integration @@ -13,7 +16,7 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam): state = hass.states.get("climate.zone_lisa_wk") attrs = state.attributes - assert attrs["hvac_modes"] == ["heat", "auto"] + assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO] assert "preset_modes" in attrs assert "no_frost" in attrs["preset_modes"] @@ -29,7 +32,7 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam): state = hass.states.get("climate.zone_thermostat_jessie") attrs = state.attributes - assert attrs["hvac_modes"] == ["heat", "auto"] + assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO] assert "preset_modes" in attrs assert "no_frost" in attrs["preset_modes"] @@ -41,6 +44,44 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam): assert attrs["preset_mode"] == "asleep" +async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam): + """Test exceptions of climate entities.""" + mock_smile_adam.set_preset.side_effect = PlugwiseException + mock_smile_adam.set_schedule_state.side_effect = PlugwiseException + mock_smile_adam.set_temperature.side_effect = PlugwiseException + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, + blocking=True, + ) + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + assert attrs["temperature"] == 21.5 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"}, + blocking=True, + ) + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + assert attrs["preset_mode"] == "asleep" + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.zone_thermostat_jessie", "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): """Test handling of user requests in adam climate device environment.""" entry = await async_init_integration(hass, mock_smile_adam) @@ -112,7 +153,7 @@ async def test_anna_climate_entity_attributes(hass, mock_smile_anna): assert attrs["current_temperature"] == 23.3 assert attrs["temperature"] == 21.0 - assert state.state == "auto" + assert state.state == HVAC_MODE_AUTO assert attrs["hvac_action"] == "idle" assert attrs["preset_mode"] == "home" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index dea42dfb01d..fc0e5f9e69f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -1,5 +1,9 @@ """Test the Plugwise config flow.""" -from Plugwise_Smile.Smile import Smile +from plugwise.exceptions import ( + ConnectionFailedError, + InvalidAuthentication, + PlugwiseException, +) import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -47,9 +51,9 @@ def mock_smile(): with patch( "homeassistant.components.plugwise.config_flow.Smile", ) as smile_mock: - smile_mock.PlugwiseError = Smile.PlugwiseError - smile_mock.InvalidAuthentication = Smile.InvalidAuthentication - smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.PlugwiseError = PlugwiseException + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.ConnectionFailedError = ConnectionFailedError smile_mock.return_value.connect.return_value = True yield smile_mock.return_value @@ -207,7 +211,7 @@ async def test_form_invalid_auth(hass, mock_smile): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_smile.connect.side_effect = Smile.InvalidAuthentication + mock_smile.connect.side_effect = InvalidAuthentication mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" result2 = await hass.config_entries.flow.async_configure( @@ -225,7 +229,7 @@ async def test_form_cannot_connect(hass, mock_smile): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_smile.connect.side_effect = Smile.ConnectionFailedError + mock_smile.connect.side_effect = ConnectionFailedError mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" result2 = await hass.config_entries.flow.async_configure( @@ -243,7 +247,7 @@ async def test_form_cannot_connect_port(hass, mock_smile): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_smile.connect.side_effect = Smile.ConnectionFailedError + mock_smile.connect.side_effect = ConnectionFailedError mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index db7a71d660b..eded1e55406 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -2,9 +2,9 @@ import asyncio -from Plugwise_Smile.Smile import Smile +from plugwise.exceptions import XMLDataMissingError -from homeassistant.components.plugwise import DOMAIN +from homeassistant.components.plugwise.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_ERROR, @@ -43,7 +43,7 @@ async def test_smile_timeout(hass, mock_smile_notconnect): async def test_smile_adam_xmlerror(hass, mock_smile_adam): """Detect malformed XML by Smile in Adam environment.""" - mock_smile_adam.full_update_device.side_effect = Smile.XMLDataMissingError + mock_smile_adam.full_update_device.side_effect = XMLDataMissingError entry = await async_init_integration(hass, mock_smile_adam) assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index ded21113f2b..b7237a26150 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -1,5 +1,7 @@ """Tests for the Plugwise switch integration.""" +from plugwise.exceptions import PlugwiseException + from homeassistant.config_entries import ENTRY_STATE_LOADED from tests.components.plugwise.common import async_init_integration @@ -17,6 +19,31 @@ async def test_adam_climate_switch_entities(hass, mock_smile_adam): assert str(state.state) == "on" +async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam): + """Test exceptions of climate related switch entities.""" + mock_smile_adam.set_relay_state.side_effect = PlugwiseException + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.cv_pomp"}, + blocking=True, + ) + state = hass.states.get("switch.cv_pomp") + assert str(state.state) == "on" + + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": "switch.fibaro_hc2"}, + blocking=True, + ) + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "on" + + async def test_adam_climate_switch_changes(hass, mock_smile_adam): """Test changing of climate related switch entities.""" entry = await async_init_integration(hass, mock_smile_adam) From b2bd68a4a0883c5fdee597202df63f1529b853d9 Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Sat, 21 Nov 2020 06:06:50 +0100 Subject: [PATCH 166/430] =?UTF-8?q?Avoid=20arbitrarily=20reducing=20ZHA?= =?UTF-8?q?=C2=A0climate=20temperature=20information=20(#43442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Zigbee specification mandates that temperatures are given in 1/100ths of degrees, and several devices are able to provide temperature readings or accept target temperature settings with precision equal or better than 0.1°C. Yet the ZHA climate entity implementation advertises an hard-coded precision of 0.5°C. Avoid arbitrary coarsening by advertising 0.1°C precision, as passing through the full 0.01°C precision is probably not that useful. --- homeassistant/components/zha/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 728d0f48217..ab0f15f7559 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -36,7 +36,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval @@ -287,7 +287,7 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def precision(self): """Return the precision of the system.""" - return PRECISION_HALVES + return PRECISION_TENTHS @property def preset_mode(self) -> Optional[str]: From 45618f80549ff4e9158f2b82cf171b0f97c7537a Mon Sep 17 00:00:00 2001 From: Alan Murray Date: Sat, 21 Nov 2020 21:00:08 +1100 Subject: [PATCH 167/430] Bump acmeda dependency aiopulse to 0.4.2 (#43217) --- homeassistant/components/acmeda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index 8b76af0c57e..f1858f9fd5a 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -3,7 +3,7 @@ "name": "Rollease Acmeda Automate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/acmeda", - "requirements": ["aiopulse==0.4.0"], + "requirements": ["aiopulse==0.4.2"], "codeowners": [ "@atmurray" ] diff --git a/requirements_all.txt b/requirements_all.txt index f687f042b1f..50a0c702bfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,7 +203,7 @@ aionotify==0.2.0 aionotion==1.1.0 # homeassistant.components.acmeda -aiopulse==0.4.0 +aiopulse==0.4.2 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d1ad87e1de..502e62b3515 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiokafka==0.6.0 aionotion==1.1.0 # homeassistant.components.acmeda -aiopulse==0.4.0 +aiopulse==0.4.2 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 From cccd4d734cd71d2c7d1dc257dd4d788055641785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kr=C3=B3l?= Date: Sat, 21 Nov 2020 11:10:11 +0100 Subject: [PATCH 168/430] Fix ConnectTimeout during wolflink start (#43418) --- homeassistant/components/wolflink/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index d04cd7a56d4..1bfae6cb900 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -2,13 +2,14 @@ from datetime import timedelta import logging -from httpcore import ConnectError, ConnectTimeout +from httpx import ConnectError, ConnectTimeout from wolf_smartset.token_auth import InvalidAuth from wolf_smartset.wolf_client import FetchFailed, WolfClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -45,7 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): wolf_client = WolfClient(username, password) - parameters = await fetch_parameters(wolf_client, gateway_id, device_id) + try: + parameters = await fetch_parameters(wolf_client, gateway_id, device_id) + except InvalidAuth: + _LOGGER.debug("Authentication failed") + return False async def async_update_data(): """Update all stored entities for Wolf SmartSet.""" @@ -103,7 +108,7 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int): try: fetched_parameters = await client.fetch_parameters(gateway_id, device_id) return [param for param in fetched_parameters if param.name != "Reglertyp"] - except (ConnectError, ConnectTimeout) as exception: - raise UpdateFailed(f"Error communicating with API: {exception}") from exception - except InvalidAuth as exception: - raise UpdateFailed("Invalid authentication during update") from exception + except (ConnectError, ConnectTimeout, FetchFailed) as exception: + raise ConfigEntryNotReady( + f"Error communicating with API: {exception}" + ) from exception From d7bf783da67659baeb1e2140db3badab4c604242 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 21 Nov 2020 10:21:23 +0000 Subject: [PATCH 169/430] Add reauth support for OVO Energy (#38882) --- .../components/ovo_energy/__init__.py | 30 +++++-- .../components/ovo_energy/config_flow.py | 49 ++++++++++- homeassistant/components/ovo_energy/const.py | 2 - homeassistant/components/ovo_energy/sensor.py | 5 +- .../components/ovo_energy/strings.json | 10 ++- .../ovo_energy/translations/en.json | 10 ++- .../components/ovo_energy/test_config_flow.py | 88 +++++++++++++++++++ 7 files changed, 176 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 445ae733ec5..0130ba30c30 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -33,23 +34,38 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool client = OVOEnergy() try: - await client.authenticate(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) + authenticated = await client.authenticate( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) except aiohttp.ClientError as exception: _LOGGER.warning(exception) raise ConfigEntryNotReady from exception + if not authenticated: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + ) + return False + async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" - now = datetime.utcnow() async with async_timeout.timeout(10): try: - await client.authenticate( + authenticated = await client.authenticate( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] ) - return await client.get_daily_usage(now.strftime("%Y-%m")) except aiohttp.ClientError as exception: - _LOGGER.warning(exception) - return None + raise UpdateFailed(exception) from exception + if not authenticated: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + ) + raise UpdateFailed("Not authenticated with OVO Energy") + return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator( hass, @@ -137,6 +153,6 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): return { "identifiers": {(DOMAIN, self._client.account_id)}, "manufacturer": "OVO Energy", - "name": self._client.account_id, + "name": self._client.username, "entry_type": "service", } diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index dfedf780592..f395415d89e 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -7,8 +7,9 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import CONF_ACCOUNT_ID, DOMAIN # pylint: disable=unused-import +from .const import DOMAIN # pylint: disable=unused-import +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) USER_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) @@ -20,6 +21,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the flow.""" + self.username = None + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} @@ -37,11 +42,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=client.account_id, + title=client.username, data={ CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_ACCOUNT_ID: client.account_id, }, ) @@ -50,3 +54,42 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=USER_SCHEMA, errors=errors ) + + async def async_step_reauth(self, user_input): + """Handle configuration by re-auth.""" + errors = {} + + if user_input and user_input.get(CONF_USERNAME): + self.username = user_input[CONF_USERNAME] + + # pylint: disable=no-member + self.context["title_placeholders"] = {CONF_USERNAME: self.username} + + if user_input is not None and user_input.get(CONF_PASSWORD) is not None: + client = OVOEnergy() + try: + authenticated = await client.authenticate( + self.username, user_input[CONF_PASSWORD] + ) + except aiohttp.ClientError: + errors["base"] = "connection_error" + else: + if authenticated: + await self.async_set_unique_id(self.username) + + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") + + errors["base"] = "authorization_error" + + return self.async_show_form( + step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/ovo_energy/const.py b/homeassistant/components/ovo_energy/const.py index e836bb2ca8a..f691eb9bc49 100644 --- a/homeassistant/components/ovo_energy/const.py +++ b/homeassistant/components/ovo_energy/const.py @@ -3,5 +3,3 @@ DOMAIN = "ovo_energy" DATA_CLIENT = "ovo_client" DATA_COORDINATOR = "coordinator" - -CONF_ACCOUNT_ID = "account_id" diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 2a64fbe2d22..2f2e1b8dd50 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -50,10 +50,7 @@ async def async_setup_entry( ) ) - async_add_entities( - entities, - True, - ) + async_add_entities(entities, True) class OVOEnergySensor(OVOEnergyDeviceEntity): diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index fac7c97bcbe..df19d4898f2 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "OVO Energy: {username}", "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", @@ -13,7 +14,14 @@ }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" + }, + "reauth": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Authentication failed for OVO Energy. Please enter your current credentials.", + "title": "Reauthentication" + } } - } } } diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 160f47ae23f..6a002b6e08a 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "OVO Energy: {username}", "error": { "already_configured": "Account is already configured", "cannot_connect": "Failed to connect", @@ -13,7 +14,14 @@ }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" + }, + "reauth": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Authentication failed for OVO Energy. Please enter your current credentials.", + "title": "Reauthentication" + } } - } } } \ No newline at end of file diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 57192933572..a7f6ea9b9f2 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -7,9 +7,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.async_mock import patch +from tests.common import MockConfigEntry +FIXTURE_REAUTH_INPUT = {CONF_PASSWORD: "something1"} FIXTURE_USER_INPUT = {CONF_USERNAME: "example@example.com", CONF_PASSWORD: "something"} +UNIQUE_ID = "example@example.com" + async def test_show_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -94,3 +98,87 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + + +async def test_reauth_authorization_error(hass: HomeAssistant) -> None: + """Test we show user form on authorization error.""" + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=False, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "authorization_error"} + + +async def test_reauth_connection_error(hass: HomeAssistant) -> None: + """Test we show user form on connection error.""" + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test reauth works.""" + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=False, + ): + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "authorization_error"} + + with patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=True, + ), patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", + return_value=FIXTURE_USER_INPUT[CONF_USERNAME], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" From 7c6e80952bb1ec1603a46ee5c3f663dfae65dd93 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Nov 2020 11:37:41 +0100 Subject: [PATCH 170/430] Upgrade debugpy to 1.2.0 (#43328) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 18583186c3b..27b110b1f68 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.1.0"], + "requirements": ["debugpy==1.2.0"], "codeowners": ["@frenck"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 50a0c702bfd..1d86132205a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -463,7 +463,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.1.0 +debugpy==1.2.0 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 502e62b3515..00a44bea9d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -245,7 +245,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.1.0 +debugpy==1.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From db0dee1ab2f939b559e07f5a44f3d0fbdaaef8b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Nov 2020 11:38:40 +0100 Subject: [PATCH 171/430] Add repeat mode support to Spotify (#43247) --- .../components/spotify/media_player.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 5f376493c65..ef3f1224a4b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -25,12 +25,16 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, + REPEAT_MODE_ALL, + REPEAT_MODE_OFF, + REPEAT_MODE_ONE, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, @@ -71,12 +75,19 @@ SUPPORT_SPOTIFY = ( | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_PREVIOUS_TRACK + | SUPPORT_REPEAT_SET | SUPPORT_SEEK | SUPPORT_SELECT_SOURCE | SUPPORT_SHUFFLE_SET | SUPPORT_VOLUME_SET ) +REPEAT_MODE_MAPPING = { + "context": REPEAT_MODE_ALL, + "off": REPEAT_MODE_OFF, + "track": REPEAT_MODE_ONE, +} + BROWSE_LIMIT = 48 MEDIA_TYPE_SHOW = "show" @@ -375,6 +386,12 @@ class SpotifyMediaPlayer(MediaPlayerEntity): """Shuffling state.""" return bool(self._currently_playing.get("shuffle_state")) + @property + def repeat(self) -> Optional[str]: + """Return current repeat mode.""" + repeat_state = self._currently_playing.get("repeat_state") + return REPEAT_MODE_MAPPING.get(repeat_state) + @property def supported_features(self) -> int: """Return the media player features that are supported.""" @@ -449,6 +466,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity): """Enable/Disable shuffle mode.""" self._spotify.shuffle(shuffle) + @spotify_exception_handler + def set_repeat(self, repeat: str) -> None: + """Set repeat mode.""" + for spotify, home_assistant in REPEAT_MODE_MAPPING.items(): + if home_assistant == repeat: + self._spotify.repeat(spotify) + @spotify_exception_handler def update(self) -> None: """Update state and attributes.""" From f22f568169c18c402bc39d3211df0509c3d4e9df Mon Sep 17 00:00:00 2001 From: FlavorFx Date: Sat, 21 Nov 2020 11:52:34 +0100 Subject: [PATCH 172/430] Fix Luftdaten.info data retrieval (#43471) Fix Luftdaten.info data retrieval --- homeassistant/components/luftdaten/__init__.py | 5 +++-- homeassistant/components/luftdaten/sensor.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 91e9c96d429..ca1b9aed4ff 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -198,8 +198,9 @@ class LuftDatenData: try: await self.client.get_data() - self.data[DATA_LUFTDATEN] = self.client.values - self.data[DATA_LUFTDATEN].update(self.client.meta) + if self.client.values: + self.data[DATA_LUFTDATEN] = self.client.values + self.data[DATA_LUFTDATEN].update(self.client.meta) except LuftdatenError: _LOGGER.error("Unable to retrieve data from luftdaten.info") diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 2d9a8fa85a4..515d8ad577f 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -69,7 +69,10 @@ class LuftdatenSensor(Entity): def state(self): """Return the state of the device.""" if self._data is not None: - return self._data[self.sensor_type] + try: + return self._data[self.sensor_type] + except KeyError: + return None @property def unit_of_measurement(self): @@ -85,7 +88,10 @@ class LuftdatenSensor(Entity): def unique_id(self) -> str: """Return a unique, friendly identifier for this entity.""" if self._data is not None: - return f"{self._data['sensor_id']}_{self.sensor_type}" + try: + return f"{self._data['sensor_id']}_{self.sensor_type}" + except KeyError: + return None @property def device_state_attributes(self): @@ -93,7 +99,10 @@ class LuftdatenSensor(Entity): self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION if self._data is not None: - self._attrs[ATTR_SENSOR_ID] = self._data["sensor_id"] + try: + self._attrs[ATTR_SENSOR_ID] = self._data["sensor_id"] + except KeyError: + return None on_map = ATTR_LATITUDE, ATTR_LONGITUDE no_map = "lat", "long" From 7213d5f31be7a8ac7765362830d6a9028477230c Mon Sep 17 00:00:00 2001 From: Indu Prakash <6459774+iprak@users.noreply.github.com> Date: Sat, 21 Nov 2020 05:44:37 -0600 Subject: [PATCH 173/430] Support for multiple states in history_stats (#43416) Co-authored-by: Indu Prakash <6459774+InduPrakash@users.noreply.github.com> --- .../components/history_stats/sensor.py | 15 ++- tests/components/history_stats/test_sensor.py | 106 ++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 8f4a89e3e37..6778e893f6f 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_STATE): cv.string, + vol.Required(CONF_STATE): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_START): cv.template, vol.Optional(CONF_END): cv.template, vol.Optional(CONF_DURATION): cv.time_period, @@ -77,11 +77,10 @@ PLATFORM_SCHEMA = vol.All( # noinspection PyUnusedLocal def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the History Stats sensor.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) entity_id = config.get(CONF_ENTITY_ID) - entity_state = config.get(CONF_STATE) + entity_states = config.get(CONF_STATE) start = config.get(CONF_START) end = config.get(CONF_END) duration = config.get(CONF_DURATION) @@ -95,7 +94,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities( [ HistoryStatsSensor( - hass, entity_id, entity_state, start, end, duration, sensor_type, name + hass, entity_id, entity_states, start, end, duration, sensor_type, name ) ] ) @@ -107,11 +106,11 @@ class HistoryStatsSensor(Entity): """Representation of a HistoryStats sensor.""" def __init__( - self, hass, entity_id, entity_state, start, end, duration, sensor_type, name + self, hass, entity_id, entity_states, start, end, duration, sensor_type, name ): """Initialize the HistoryStats sensor.""" self._entity_id = entity_id - self._entity_state = entity_state + self._entity_states = entity_states self._duration = duration self._start = start self._end = end @@ -230,14 +229,14 @@ class HistoryStatsSensor(Entity): # Get the first state last_state = history.get_state(self.hass, start, self._entity_id) - last_state = last_state is not None and last_state == self._entity_state + last_state = last_state is not None and last_state in self._entity_states last_time = start_timestamp elapsed = 0 count = 0 # Make calculations for item in history_list.get(self._entity_id): - current_state = item.state == self._entity_state + current_state = item.state in self._entity_states current_time = item.last_changed.timestamp() if last_state: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index bab6eae4564..db6d7476912 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -50,6 +50,28 @@ class TestHistoryStatsSensor(unittest.TestCase): state = self.hass.states.get("sensor.test") assert state.state == STATE_UNKNOWN + def test_setup_multiple_states(self): + """Test the history statistics sensor setup for multiple states.""" + self.init_recorder() + config = { + "history": {}, + "sensor": { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "state": ["on", "true"], + "start": "{{ now().replace(hour=0)" + ".replace(minute=0).replace(second=0) }}", + "duration": "02:00", + "name": "Test", + }, + } + + assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, @@ -152,6 +174,90 @@ class TestHistoryStatsSensor(unittest.TestCase): assert sensor3.state == 2 assert sensor4.state == 50 + def test_measure_multiple(self): + """Test the history statistics sensor measure for multiple states.""" + t0 = dt_util.utcnow() - timedelta(minutes=40) + t1 = t0 + timedelta(minutes=20) + t2 = dt_util.utcnow() - timedelta(minutes=10) + + # Start t0 t1 t2 End + # |--20min--|--20min--|--10min--|--10min--| + # |---------|--orange-|-default-|---blue--| + + fake_states = { + "input_select.test_id": [ + ha.State("input_select.test_id", "orange", last_changed=t0), + ha.State("input_select.test_id", "default", last_changed=t1), + ha.State("input_select.test_id", "blue", last_changed=t2), + ] + } + + start = Template("{{ as_timestamp(now()) - 3600 }}", self.hass) + end = Template("{{ now() }}", self.hass) + + sensor1 = HistoryStatsSensor( + self.hass, + "input_select.test_id", + ["orange", "blue"], + start, + end, + None, + "time", + "Test", + ) + + sensor2 = HistoryStatsSensor( + self.hass, + "unknown.id", + ["orange", "blue"], + start, + end, + None, + "time", + "Test", + ) + + sensor3 = HistoryStatsSensor( + self.hass, + "input_select.test_id", + ["orange", "blue"], + start, + end, + None, + "count", + "test", + ) + + sensor4 = HistoryStatsSensor( + self.hass, + "input_select.test_id", + ["orange", "blue"], + start, + end, + None, + "ratio", + "test", + ) + + assert sensor1._type == "time" + assert sensor3._type == "count" + assert sensor4._type == "ratio" + + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ): + with patch("homeassistant.components.history.get_state", return_value=None): + sensor1.update() + sensor2.update() + sensor3.update() + sensor4.update() + + assert sensor1.state == 0.5 + assert sensor2.state is None + assert sensor3.state == 2 + assert sensor4.state == 50 + def test_wrong_date(self): """Test when start or end value is not a timestamp or a date.""" good = Template("{{ now() }}", self.hass) From c5803614e18f72fe16b65b96999691b19fec9207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kol=C3=A1=C5=99?= Date: Sat, 21 Nov 2020 13:07:17 +0100 Subject: [PATCH 174/430] Upgrade discord.py to 1.5.1 (#43473) * Upgrade discord.py to 1.5.1 discord.py 1.5.1 introduces support for Intents that are required by future bots and integrations. While this is not yet required by Home Assistant Discord integration it is needed in the future, because old API which discord.py 1.4.1 uses is now deprecated. Also it would solve dependency collision with my custom component discord_game. * Upgrade discord.py to 1.5.1 --- 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 ccaa595f126..88bebe509b7 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,6 +2,6 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.4.1"], + "requirements": ["discord.py==1.5.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 1d86132205a..914968cab6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ directv==0.4.0 discogs_client==2.3.0 # homeassistant.components.discord -discord.py==1.4.1 +discord.py==1.5.1 # homeassistant.components.updater distro==1.5.0 From ca053d44992fbff824767d8c40467e3df883208c Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Sat, 21 Nov 2020 04:21:51 -0800 Subject: [PATCH 175/430] Bump rpi-bad-power to 0.1.0 (#43476) --- homeassistant/components/rpi_power/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json index e0d2a6424e8..1b355711535 100644 --- a/homeassistant/components/rpi_power/manifest.json +++ b/homeassistant/components/rpi_power/manifest.json @@ -7,7 +7,7 @@ "@swetoast" ], "requirements": [ - "rpi-bad-power==0.0.3" + "rpi-bad-power==0.1.0" ], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 914968cab6b..62e5987ed94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1960,7 +1960,7 @@ roonapi==0.0.25 rova==0.1.0 # homeassistant.components.rpi_power -rpi-bad-power==0.0.3 +rpi-bad-power==0.1.0 # homeassistant.components.rpi_rf # rpi-rf==0.9.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00a44bea9d7..4c4b201b742 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ roombapy==1.6.1 roonapi==0.0.25 # homeassistant.components.rpi_power -rpi-bad-power==0.0.3 +rpi-bad-power==0.1.0 # homeassistant.components.yamaha rxv==0.6.0 From a092b4c2041bc5b827d42f1492ea5a480ba91cfc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Nov 2020 16:32:55 +0100 Subject: [PATCH 176/430] Ensure Plex content_id in play_on_sonos service is a string (#43483) --- 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 32ee5c56f64..e4f4f80dcfa 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass, entry): play_on_sonos_schema = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_MEDIA_CONTENT_ID): str, + vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, vol.Optional(ATTR_MEDIA_CONTENT_TYPE): vol.In("music"), } ) From 977ed942ba3515ebd9493aa444fb3a85f8218b97 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sat, 21 Nov 2020 17:50:46 +0100 Subject: [PATCH 177/430] Deprecate YAML config for Solar-log (#43484) Per ADR-0010, this PR deprecates YAML configuration for Solar-log. Users who already use the Solar-log integration do not need to take action, as their configuration has already been imported into the UI. --- homeassistant/components/solarlog/sensor.py | 35 +++++++-------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index ca9f2e3fc13..ef6d050b264 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -4,39 +4,28 @@ from urllib.parse import ParseResult, urlparse from requests.exceptions import HTTPError, Timeout from sunwatcher.solarlog.solarlog import SolarLog -import voluptuous as vol -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.const import CONF_HOST from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN, SCAN_INTERVAL, SENSOR_TYPES +from .const import 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) - ) + """Set up the solarlog platform.""" + _LOGGER.warning( + "Configuration of the solarlog platform in configuration.yaml is deprecated in Home Assistant 0.119. Please remove entry from your configuration" ) + return True async def async_setup_entry(hass, entry, async_add_entities): """Add solarlog entry.""" host_entry = entry.data[CONF_HOST] + device_name = entry.title url = urlparse(host_entry, "http") netloc = url.netloc or url.path @@ -44,8 +33,6 @@ async def async_setup_entry(hass, entry, async_add_entities): 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") @@ -61,7 +48,7 @@ async def async_setup_entry(hass, entry, async_add_entities): # Create a new sensor for each sensor type. entities = [] for sensor_key in SENSOR_TYPES: - sensor = SolarlogSensor(entry.entry_id, platform_name, sensor_key, data) + sensor = SolarlogSensor(entry.entry_id, device_name, sensor_key, data) entities.append(sensor) async_add_entities(entities, True) @@ -71,9 +58,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class SolarlogSensor(Entity): """Representation of a Sensor.""" - def __init__(self, entry_id, platform_name, sensor_key, data): + def __init__(self, entry_id, device_name, sensor_key, data): """Initialize the sensor.""" - self.platform_name = platform_name + self.device_name = device_name self.sensor_key = sensor_key self.data = data self.entry_id = entry_id @@ -92,7 +79,7 @@ class SolarlogSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return f"{self.platform_name} {self._label}" + return f"{self.device_name} {self._label}" @property def unit_of_measurement(self): From 76eb5aeeb61077dac675271c6ed8e8347fe7dc91 Mon Sep 17 00:00:00 2001 From: Crash Date: Sat, 21 Nov 2020 11:24:04 -0800 Subject: [PATCH 178/430] Add updated British Voices (#43496) Say hello to Charlotte and James. https://cloud.ibm.com/apidocs/text-to-speech?code=python#getvoice --- homeassistant/components/watson_tts/tts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 27bfff9550b..9b2af2ea7fe 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -26,6 +26,8 @@ SUPPORTED_VOICES = [ "de-DE_ErikaV3Voice", "en-GB_KateV3Voice", "en-GB_KateVoice", + "en-GB_CharlotteV3Voice", + "en-GB_JamesV3Voice", "en-US_AllisonV3Voice", "en-US_AllisonVoice", "en-US_EmilyV3Voice", From b2c9bd2ca67df277093790e3fc0def34d9f7fb04 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 21 Nov 2020 21:47:57 +0200 Subject: [PATCH 179/430] Gracefully handle no uuid in kodi discovery (#43494) --- homeassistant/components/kodi/config_flow.py | 5 ++++- homeassistant/components/kodi/strings.json | 3 ++- tests/components/kodi/test_config_flow.py | 11 +++++++++++ tests/components/kodi/util.py | 10 ++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index c11255aba87..c48e4564f92 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -104,7 +104,10 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = discovery_info["host"] self._port = int(discovery_info["port"]) self._name = discovery_info["hostname"][: -len(".local.")] - uuid = discovery_info["properties"]["uuid"] + uuid = discovery_info["properties"].get("uuid") + if not uuid: + return self.async_abort(reason="no_uuid") + self._discovery_name = discovery_info["name"] await self.async_set_unique_id(uuid) diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json index cf2f265f577..f1bb5342903 100644 --- a/homeassistant/components/kodi/strings.json +++ b/homeassistant/components/kodi/strings.json @@ -37,7 +37,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_uuid": "Kodi instance does not have a unique id. This is most likely due to an old Kodi version (17.x or below). You can configure the integration manually or upgrade to a more recent Kodi version." } }, "device_automation": { diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 9b010f3ed42..9e892033786 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.kodi.const import DEFAULT_TIMEOUT, DOMAIN from .util import ( TEST_CREDENTIALS, TEST_DISCOVERY, + TEST_DISCOVERY_WO_UUID, TEST_HOST, TEST_IMPORT, TEST_WS_PORT, @@ -573,6 +574,16 @@ async def test_discovery_updates_unique_id(hass): assert entry.data["name"] == "hostname" +async def test_discovery_without_unique_id(hass): + """Test a discovery flow with no unique id aborts.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY_WO_UUID + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_uuid" + + async def test_form_import(hass): """Test we get the form with import source.""" with patch( diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index 5a47ea88631..edd6950d76e 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -24,6 +24,16 @@ TEST_DISCOVERY = { } +TEST_DISCOVERY_WO_UUID = { + "host": "1.1.1.1", + "port": 8080, + "hostname": "hostname.local.", + "type": "_xbmc-jsonrpc-h._tcp.local.", + "name": "hostname._xbmc-jsonrpc-h._tcp.local.", + "properties": {}, +} + + TEST_IMPORT = { "name": "name", "host": "1.1.1.1", From acca35cdc48c959e8416664af17973e2404d79cc Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 22 Nov 2020 00:04:54 +0000 Subject: [PATCH 180/430] [ci skip] Translation update --- .../binary_sensor/translations/ru.json | 4 +++ .../cloudflare/translations/ka.json | 30 +++++++++++++++++++ .../dialogflow/translations/ka.json | 1 + .../forked_daapd/translations/ka.json | 7 +++++ .../components/geofency/translations/ka.json | 1 + .../components/gpslogger/translations/ka.json | 1 + .../components/ifttt/translations/ka.json | 1 + .../islamic_prayer_times/translations/ka.json | 7 +++++ .../components/kodi/translations/cs.json | 1 + .../components/kodi/translations/en.json | 1 + .../components/local_ip/translations/es.json | 1 + .../components/local_ip/translations/et.json | 1 + .../components/local_ip/translations/pl.json | 1 + .../components/local_ip/translations/ru.json | 1 + .../local_ip/translations/zh-Hant.json | 1 + .../components/locative/translations/ka.json | 1 + .../components/luftdaten/translations/ka.json | 8 +++++ .../components/mailgun/translations/ka.json | 1 + .../components/nest/translations/ka.json | 7 +++++ .../components/onewire/translations/ka.json | 26 ++++++++++++++++ .../opentherm_gw/translations/ka.json | 7 +++++ .../ovo_energy/translations/cs.json | 8 +++++ .../ovo_energy/translations/en.json | 18 +++++------ .../ovo_energy/translations/es.json | 8 +++++ .../ovo_energy/translations/et.json | 8 +++++ .../ovo_energy/translations/ru.json | 8 +++++ .../components/owntracks/translations/ka.json | 7 +++++ .../components/ozw/translations/ka.json | 3 +- .../rainmachine/translations/ka.json | 3 ++ .../srp_energy/translations/es.json | 24 +++++++++++++++ .../srp_energy/translations/pl.json | 24 +++++++++++++++ .../srp_energy/translations/ru.json | 24 +++++++++++++++ .../srp_energy/translations/zh-Hant.json | 24 +++++++++++++++ .../components/tuya/translations/ka.json | 14 +++++++++ .../twinkly/translations/zh-Hant.json | 19 ++++++++++++ 35 files changed, 291 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/cloudflare/translations/ka.json create mode 100644 homeassistant/components/forked_daapd/translations/ka.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/ka.json create mode 100644 homeassistant/components/luftdaten/translations/ka.json create mode 100644 homeassistant/components/nest/translations/ka.json create mode 100644 homeassistant/components/onewire/translations/ka.json create mode 100644 homeassistant/components/opentherm_gw/translations/ka.json create mode 100644 homeassistant/components/owntracks/translations/ka.json create mode 100644 homeassistant/components/srp_energy/translations/es.json create mode 100644 homeassistant/components/srp_energy/translations/pl.json create mode 100644 homeassistant/components/srp_energy/translations/ru.json create mode 100644 homeassistant/components/srp_energy/translations/zh-Hant.json create mode 100644 homeassistant/components/twinkly/translations/zh-Hant.json diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 6d2dd417a94..8a192c3dbe0 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -126,6 +126,10 @@ "off": "\u041d\u043e\u0440\u043c\u0430", "on": "\u041d\u0430\u0433\u0440\u0435\u0432" }, + "light": { + "off": "\u0412\u044b\u043a\u043b", + "on": "\u0412\u043a\u043b" + }, "lock": { "off": "\u0417\u0430\u043a\u0440\u044b\u0442", "on": "\u041e\u0442\u043a\u0440\u044b\u0442" diff --git a/homeassistant/components/cloudflare/translations/ka.json b/homeassistant/components/cloudflare/translations/ka.json new file mode 100644 index 00000000000..349dbc81682 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/ka.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "invalid_auth": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", + "invalid_zone": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d6\u10dd\u10dc\u10d0" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "\u10e9\u10d0\u10dc\u10d0\u10ec\u10d4\u10e0\u10d4\u10d1\u10d8" + }, + "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10e9\u10d0\u10dc\u10d0\u10ec\u10d4\u10e0\u10d4\u10d1\u10d8 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1" + }, + "user": { + "data": { + "api_token": "API \u10e2\u10dd\u10d9\u10d4\u10dc\u10d8" + }, + "description": "\u10d4\u10e1 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d0 \u10db\u10dd\u10d8\u10d7\u10ee\u10dd\u10d5\u10e1 API \u10e2\u10dd\u10d9\u10d4\u10dc\u10e1, \u10e0\u10dd\u10db\u10d4\u10da\u10d8\u10ea \u10e8\u10d4\u10e5\u10db\u10dc\u10d8\u10da\u10d8\u10d0 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 Zone: Zone: Read \u10d3\u10d0 Zone: DNS:Edit \u10dc\u10d4\u10d1\u10d0\u10e0\u10d7\u10d5\u10d4\u10d1\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10d6\u10dd\u10dc\u10d4\u10d1\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1", + "title": "Cloudflare- \u10e1\u10d7\u10d0\u10dc \u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8" + }, + "zone": { + "data": { + "zone": "\u10d6\u10dd\u10dc\u10d0" + }, + "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d6\u10dd\u10dc\u10d0 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/ka.json b/homeassistant/components/dialogflow/translations/ka.json index a284a55fbcf..75c4f0a922c 100644 --- a/homeassistant/components/dialogflow/translations/ka.json +++ b/homeassistant/components/dialogflow/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.", "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." } } diff --git a/homeassistant/components/forked_daapd/translations/ka.json b/homeassistant/components/forked_daapd/translations/ka.json new file mode 100644 index 00000000000..b02d04823e5 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "forbidden": "\u1c95\u10d4\u10e0 \u10d5\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d4\u10d1\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7, \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10dd\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 forked-daapd \u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10e3\u10e4\u10da\u10d4\u10d1\u10d4\u10d1\u10d8." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ka.json b/homeassistant/components/geofency/translations/ka.json index a284a55fbcf..75c4f0a922c 100644 --- a/homeassistant/components/geofency/translations/ka.json +++ b/homeassistant/components/geofency/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.", "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." } } diff --git a/homeassistant/components/gpslogger/translations/ka.json b/homeassistant/components/gpslogger/translations/ka.json index a284a55fbcf..75c4f0a922c 100644 --- a/homeassistant/components/gpslogger/translations/ka.json +++ b/homeassistant/components/gpslogger/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.", "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." } } diff --git a/homeassistant/components/ifttt/translations/ka.json b/homeassistant/components/ifttt/translations/ka.json index a284a55fbcf..75c4f0a922c 100644 --- a/homeassistant/components/ifttt/translations/ka.json +++ b/homeassistant/components/ifttt/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.", "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." } } diff --git a/homeassistant/components/islamic_prayer_times/translations/ka.json b/homeassistant/components/islamic_prayer_times/translations/ka.json new file mode 100644 index 00000000000..503f471efb3 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/cs.json b/homeassistant/components/kodi/translations/cs.json index e21c08b0758..ccfb08328fb 100644 --- a/homeassistant/components/kodi/translations/cs.json +++ b/homeassistant/components/kodi/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "no_uuid": "Instance Kodi nem\u00e1 jedine\u010dn\u00e9 ID. To je pravd\u011bpodobn\u011b zp\u016fsobeno starou verz\u00ed Kodi (17.x nebo ni\u017e\u0161\u00ed). Integraci m\u016f\u017eete nastavit ru\u010dn\u011b nebo aktualizovat na nov\u011bj\u0161\u00ed verzi Kodi.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { diff --git a/homeassistant/components/kodi/translations/en.json b/homeassistant/components/kodi/translations/en.json index be87d312146..be6af6a7f91 100644 --- a/homeassistant/components/kodi/translations/en.json +++ b/homeassistant/components/kodi/translations/en.json @@ -4,6 +4,7 @@ "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "no_uuid": "Kodi instance does not have a unique id. This is most likely due to an old Kodi version (17.x or below). You can configure the integration manually or upgrade to a more recent Kodi version.", "unknown": "Unexpected error" }, "error": { diff --git a/homeassistant/components/local_ip/translations/es.json b/homeassistant/components/local_ip/translations/es.json index 570a566d033..fe9a0ad1414 100644 --- a/homeassistant/components/local_ip/translations/es.json +++ b/homeassistant/components/local_ip/translations/es.json @@ -8,6 +8,7 @@ "data": { "name": "Nombre del sensor" }, + "description": "\u00bfQuieres empezar a configurar?", "title": "Direcci\u00f3n IP local" } } diff --git a/homeassistant/components/local_ip/translations/et.json b/homeassistant/components/local_ip/translations/et.json index 7bd74eff99c..63e9e251d30 100644 --- a/homeassistant/components/local_ip/translations/et.json +++ b/homeassistant/components/local_ip/translations/et.json @@ -8,6 +8,7 @@ "data": { "name": "Anduri nimi" }, + "description": "Kas soovid alustada seadistamist?", "title": "Kohalik IP-aadress" } } diff --git a/homeassistant/components/local_ip/translations/pl.json b/homeassistant/components/local_ip/translations/pl.json index 2002b250883..eab29842291 100644 --- a/homeassistant/components/local_ip/translations/pl.json +++ b/homeassistant/components/local_ip/translations/pl.json @@ -8,6 +8,7 @@ "data": { "name": "Nazwa sensora" }, + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?", "title": "Lokalny adres IP" } } diff --git a/homeassistant/components/local_ip/translations/ru.json b/homeassistant/components/local_ip/translations/ru.json index 15648bf3f7f..afa78a42778 100644 --- a/homeassistant/components/local_ip/translations/ru.json +++ b/homeassistant/components/local_ip/translations/ru.json @@ -8,6 +8,7 @@ "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" } } diff --git a/homeassistant/components/local_ip/translations/zh-Hant.json b/homeassistant/components/local_ip/translations/zh-Hant.json index 3ab8f2b7592..d0238ff7436 100644 --- a/homeassistant/components/local_ip/translations/zh-Hant.json +++ b/homeassistant/components/local_ip/translations/zh-Hant.json @@ -8,6 +8,7 @@ "data": { "name": "\u50b3\u611f\u5668\u540d\u7a31" }, + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", "title": "\u672c\u5730 IP \u4f4d\u5740" } } diff --git a/homeassistant/components/locative/translations/ka.json b/homeassistant/components/locative/translations/ka.json index a284a55fbcf..75c4f0a922c 100644 --- a/homeassistant/components/locative/translations/ka.json +++ b/homeassistant/components/locative/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.", "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." } } diff --git a/homeassistant/components/luftdaten/translations/ka.json b/homeassistant/components/luftdaten/translations/ka.json new file mode 100644 index 00000000000..c6fa9829942 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/ka.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "already_configured": "\u10e1\u10d4\u10e0\u10d5\u10d8\u10e1\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ka.json b/homeassistant/components/mailgun/translations/ka.json index a284a55fbcf..75c4f0a922c 100644 --- a/homeassistant/components/mailgun/translations/ka.json +++ b/homeassistant/components/mailgun/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.", "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3." } } diff --git a/homeassistant/components/nest/translations/ka.json b/homeassistant/components/nest/translations/ka.json new file mode 100644 index 00000000000..dfe103d5253 --- /dev/null +++ b/homeassistant/components/nest/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_url_available": "URL \u10db\u10d8\u10e3\u10ec\u10d5\u10d3\u10dd\u10db\u10d4\u10da\u10d8\u10d0. \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d0\u10e1 \u10d0\u10db \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 , [\u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 help \u10e1\u10d4\u10e5\u10ea\u10d8\u10d0] ({docs_url})" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ka.json b/homeassistant/components/onewire/translations/ka.json new file mode 100644 index 00000000000..1b3c7e8ef5c --- /dev/null +++ b/homeassistant/components/onewire/translations/ka.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", + "invalid_path": "\u10d3\u10d8\u10e0\u10d4\u10e5\u10e2\u10dd\u10e0\u10d8\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0." + }, + "step": { + "owserver": { + "data": { + "host": "\u10f0\u10dd\u10e1\u10e2\u10d8", + "port": "\u10de\u10dd\u10e0\u10e2\u10d8" + }, + "title": "owserver \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" + }, + "user": { + "data": { + "type": "\u1c99\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8" + }, + "title": "1-\u10db\u10d0\u10d5\u10d7\u10d8\u10da\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ka.json b/homeassistant/components/opentherm_gw/translations/ka.json new file mode 100644 index 00000000000..01c8a2e4811 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/cs.json b/homeassistant/components/ovo_energy/translations/cs.json index d8276248ad4..34dd05dc8bf 100644 --- a/homeassistant/components/ovo_energy/translations/cs.json +++ b/homeassistant/components/ovo_energy/translations/cs.json @@ -5,7 +5,15 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Heslo" + }, + "description": "Ov\u011b\u0159en\u00ed pro OVO Energy se nezda\u0159ilo. Zadejte sv\u00e9 aktu\u00e1ln\u00ed p\u0159ihla\u0161ovac\u00ed \u00fadaje.", + "title": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 6a002b6e08a..7b3160af97e 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -1,12 +1,19 @@ { "config": { - "flow_title": "OVO Energy: {username}", "error": { "already_configured": "Account is already configured", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "Authentication failed for OVO Energy. Please enter your current credentials.", + "title": "Reauthentication" + }, "user": { "data": { "password": "Password", @@ -14,14 +21,7 @@ }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" - }, - "reauth": { - "data": { - "password": "[%key:common::config_flow::data::password%]" - }, - "description": "Authentication failed for OVO Energy. Please enter your current credentials.", - "title": "Reauthentication" - } } + } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index 43f2d7e4541..aa9708c60d0 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -5,7 +5,15 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Error de autenticaci\u00f3n para OVO Energy. Ingrese sus credenciales actuales.", + "title": "Reautenticaci\u00f3n" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/ovo_energy/translations/et.json b/homeassistant/components/ovo_energy/translations/et.json index bc3df1eaf44..b91f360159a 100644 --- a/homeassistant/components/ovo_energy/translations/et.json +++ b/homeassistant/components/ovo_energy/translations/et.json @@ -5,7 +5,15 @@ "cannot_connect": "\u00dchendus nurjus", "invalid_auth": "Tuvastamise viga" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "OVO Energy autentimine nurjus. Sisesta oma praegused volitused.", + "title": "Taastuvastamine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index 08f2ce89b0d..dd422bac01f 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -5,7 +5,15 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/owntracks/translations/ka.json b/homeassistant/components/owntracks/translations/ka.json new file mode 100644 index 00000000000..503f471efb3 --- /dev/null +++ b/homeassistant/components/owntracks/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ka.json b/homeassistant/components/ozw/translations/ka.json index 6121721be50..da587087c92 100644 --- a/homeassistant/components/ozw/translations/ka.json +++ b/homeassistant/components/ozw/translations/ka.json @@ -3,7 +3,8 @@ "abort": { "addon_info_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e4\u10dd\u10e1 \u10db\u10d8\u10e6\u10d4\u10d1\u10d0.", "addon_install_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10d0.", - "addon_set_config_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0." + "addon_set_config_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0.", + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." }, "error": { "addon_start_failed": "OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8 \u10d0\u10e0 \u10d3\u10d0\u10d8\u10e1\u10e2\u10d0\u10e0\u10e2\u10d0. \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." diff --git a/homeassistant/components/rainmachine/translations/ka.json b/homeassistant/components/rainmachine/translations/ka.json index 0c7e50133f4..cb1c9a8336b 100644 --- a/homeassistant/components/rainmachine/translations/ka.json +++ b/homeassistant/components/rainmachine/translations/ka.json @@ -2,6 +2,9 @@ "options": { "step": { "init": { + "data": { + "zone_run_time": "\u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8 \u10d6\u10dd\u10dc\u10d8\u10e1 \u10d2\u10d0\u10e8\u10d5\u10d4\u10d1\u10d8\u10e1 \u10d3\u10e0\u10dd (\u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8)" + }, "title": "RainMachine-\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0" } } diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json new file mode 100644 index 00000000000..de15bb80551 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_account": "El ID de la cuenta debe ser un n\u00famero de 9 d\u00edgitos", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "id": "ID de la cuenta", + "is_tou": "Es el plan de tiempo de uso", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pl.json b/homeassistant/components/srp_energy/translations/pl.json new file mode 100644 index 00000000000..f89165a9065 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_account": "Identyfikator konta powinien sk\u0142ada\u0107 si\u0119 z 9 cyfr", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "id": "Identyfikator konta", + "is_tou": "Taryfa dzie\u0144/noc?", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ru.json b/homeassistant/components/srp_energy/translations/ru.json new file mode 100644 index 00000000000..3fcbace37df --- /dev/null +++ b/homeassistant/components/srp_energy/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_account": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c 9-\u0437\u043d\u0430\u0447\u043d\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "id": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430", + "is_tou": "\u041f\u043b\u0430\u043d \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/zh-Hant.json b/homeassistant/components/srp_energy/translations/zh-Hant.json new file mode 100644 index 00000000000..f8cb25f7df5 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_account": "\u5e33\u865f ID \u5fc5\u9808\u70ba 9 \u4f4d\u6578\u5b57", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "id": "\u5e33\u865f ID", + "is_tou": "\u662f\u5426\u70ba Time of Use \u65b9\u6848", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ka.json b/homeassistant/components/tuya/translations/ka.json index 4418dc65d35..7c80ef1ffba 100644 --- a/homeassistant/components/tuya/translations/ka.json +++ b/homeassistant/components/tuya/translations/ka.json @@ -1,8 +1,22 @@ { "options": { + "error": { + "dev_multi_type": "\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8 \u10db\u10e0\u10d0\u10d5\u10da\u10dd\u10d1\u10d8\u10d7\u10d8 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10dc\u10d3\u10d0 \u10d8\u10e7\u10dd\u10e1 \u10d4\u10e0\u10d7\u10dc\u10d0\u10d8\u10e0\u10d8 \u10e2\u10d8\u10de\u10d8\u10e1", + "dev_not_config": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0\u10d3\u10d8", + "dev_not_found": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0" + }, "step": { "device": { "data": { + "brightness_range_mode": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e1\u10d8\u10d9\u10d0\u10e8\u10d9\u10d0\u10e8\u10d8\u10e1 \u10d3\u10d8\u10d0\u10de\u10d0\u10d6\u10dd\u10dc\u10d8", + "curr_temp_divider": "\u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4 \u10e2\u10d4\u10db\u10d4\u10de\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 - \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)", + "max_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8", + "max_temp": "\u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)", + "min_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8", + "min_temp": "\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)", + "support_color": "\u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d0 \u10d8\u10eb\u10e3\u10da\u10d4\u10d1\u10d8\u10d7", + "temp_divider": "\u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 = \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)", + "tuya_max_coltemp": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10db\u10dd\u10ec\u10dd\u10d3\u10d4\u10d1\u10e3\u10da\u10d8 \u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0", "unit_of_measurement": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10d4\u10e0\u10d7\u10d4\u10e3\u10da\u10d8" }, "description": "\u10d3\u10d0\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d3 {device_type} `{device_name}` \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10d4\u10e0\u10d1\u10d8 \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e9\u10d5\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10db\u10dd\u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10d0\u10d3", diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json new file mode 100644 index 00000000000..a325d458acb --- /dev/null +++ b/homeassistant/components/twinkly/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "Twinkly \u8a2d\u5099\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09" + }, + "description": "\u8a2d\u5b9a Twinkly LED \u71c8\u4e32", + "title": "Twinkly" + } + } + } +} \ No newline at end of file From a0a44f12a740958dd778d26325205eab604e5ed5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Nov 2020 05:40:31 +0100 Subject: [PATCH 181/430] Clean up Solar-log review comments (#43503) --- homeassistant/components/solarlog/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index ef6d050b264..8c0650f0f7e 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -17,9 +17,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the solarlog platform.""" _LOGGER.warning( - "Configuration of the solarlog platform in configuration.yaml is deprecated in Home Assistant 0.119. Please remove entry from your configuration" + "Configuration of the solarlog platform in configuration.yaml is deprecated " + "in Home Assistant 0.119. Please remove entry from your configuration" ) - return True async def async_setup_entry(hass, entry, async_add_entities): From d7e696b90a526bc4499517c8f7606d71e68f7f4d Mon Sep 17 00:00:00 2001 From: ahertz Date: Sun, 22 Nov 2020 06:21:27 -0500 Subject: [PATCH 182/430] Bump sleepyq to 0.8.1 (#43505) --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 44e519f57da..0f5064f3264 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -2,6 +2,6 @@ "domain": "sleepiq", "name": "SleepIQ", "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["sleepyq==0.7"], + "requirements": ["sleepyq==0.8.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 62e5987ed94..1b062639a2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2030,7 +2030,7 @@ skybellpy==0.6.1 slackclient==2.5.0 # homeassistant.components.sleepiq -sleepyq==0.7 +sleepyq==0.8.1 # homeassistant.components.xmpp slixmpp==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c4b201b742..9d647c54678 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -981,7 +981,7 @@ simplisafe-python==9.6.0 slackclient==2.5.0 # homeassistant.components.sleepiq -sleepyq==0.7 +sleepyq==0.8.1 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 From 7319c4692d1b34e0741978755d532f3cba6adc54 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 22 Nov 2020 12:25:13 +0100 Subject: [PATCH 183/430] Optimize Sonos queue position (#43514) --- homeassistant/components/sonos/media_player.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index cd36ee82b51..e3c24260a2e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -529,7 +529,6 @@ class SonosEntity(MediaPlayerEntity): self._media_artist = None self._media_album_name = None self._media_title = None - self._is_playing_local_queue = None self._queue_position = None self._night_sound = None self._speech_enhance = None @@ -740,13 +739,12 @@ class SonosEntity(MediaPlayerEntity): self._media_artist = None self._media_album_name = None self._media_title = None + self._queue_position = None self._source_name = None update_position = new_status != self._status self._status = new_status - self._is_playing_local_queue = self.soco.is_playing_local_queue - if self.soco.is_playing_tv: self.update_media_linein(SOURCE_TV) elif self.soco.is_playing_line_in: @@ -849,7 +847,9 @@ class SonosEntity(MediaPlayerEntity): self._media_image_url = track_info.get("album_art") - self._queue_position = int(track_info.get("playlist_position")) - 1 + playlist_position = int(track_info.get("playlist_position")) + if playlist_position > 0: + self._queue_position = playlist_position - 1 def update_volume(self, event=None): """Update information about currently volume settings.""" @@ -1038,10 +1038,7 @@ class SonosEntity(MediaPlayerEntity): @soco_coordinator def queue_position(self): """If playing local queue return the position in the queue else None.""" - if self._is_playing_local_queue: - return self._queue_position - - return None + return self._queue_position @property @soco_coordinator From be39104a366a14522d480fd124be13137fe19631 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 22 Nov 2020 04:50:22 -0700 Subject: [PATCH 184/430] Re-organize OpenUV constants (#43453) Co-authored-by: Franck Nijhof --- homeassistant/components/openuv/__init__.py | 51 +++++++------------ .../components/openuv/binary_sensor.py | 15 +++--- homeassistant/components/openuv/const.py | 20 ++++++++ homeassistant/components/openuv/sensor.py | 14 ++--- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 5161011dd4c..ce75365771d 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,6 +1,5 @@ """Support for UV data from openuv.io.""" import asyncio -import logging from pyopenuv import Client from pyopenuv.errors import OpenUvError @@ -24,14 +23,14 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DATA_OPENUV_CLIENT = "data_client" -DATA_OPENUV_LISTENER = "data_listener" -DATA_PROTECTION_WINDOW = "protection_window" -DATA_UV = "uv" +from .const import ( + DATA_CLIENT, + DATA_LISTENER, + DATA_PROTECTION_WINDOW, + DATA_UV, + DOMAIN, + LOGGER, +) DEFAULT_ATTRIBUTION = "Data provided by OpenUV" @@ -40,24 +39,12 @@ NOTIFICATION_TITLE = "OpenUV Component Setup" TOPIC_UPDATE = f"{DOMAIN}_data_update" -TYPE_CURRENT_OZONE_LEVEL = "current_ozone_level" -TYPE_CURRENT_UV_INDEX = "current_uv_index" -TYPE_CURRENT_UV_LEVEL = "current_uv_level" -TYPE_MAX_UV_INDEX = "max_uv_index" -TYPE_PROTECTION_WINDOW = "uv_protection_window" -TYPE_SAFE_EXPOSURE_TIME_1 = "safe_exposure_time_type_1" -TYPE_SAFE_EXPOSURE_TIME_2 = "safe_exposure_time_type_2" -TYPE_SAFE_EXPOSURE_TIME_3 = "safe_exposure_time_type_3" -TYPE_SAFE_EXPOSURE_TIME_4 = "safe_exposure_time_type_4" -TYPE_SAFE_EXPOSURE_TIME_5 = "safe_exposure_time_type_5" -TYPE_SAFE_EXPOSURE_TIME_6 = "safe_exposure_time_type_6" - PLATFORMS = ["binary_sensor", "sensor"] async def async_setup(hass, config): """Set up the OpenUV component.""" - hass.data[DOMAIN] = {DATA_OPENUV_CLIENT: {}, DATA_OPENUV_LISTENER: {}} + hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} return True @@ -77,9 +64,9 @@ async def async_setup_entry(hass, config_entry): ) ) await openuv.async_update() - hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = openuv except OpenUvError as err: - _LOGGER.error("Config entry failed: %s", err) + LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err for component in PLATFORMS: @@ -90,21 +77,21 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def update_data(service): """Refresh all OpenUV data.""" - _LOGGER.debug("Refreshing all OpenUV data") + LOGGER.debug("Refreshing all OpenUV data") await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control async def update_uv_index_data(service): """Refresh OpenUV UV index data.""" - _LOGGER.debug("Refreshing OpenUV UV index data") + LOGGER.debug("Refreshing OpenUV UV index data") await openuv.async_update_uv_index_data() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control async def update_protection_data(service): """Refresh OpenUV protection window data.""" - _LOGGER.debug("Refreshing OpenUV protection window data") + LOGGER.debug("Refreshing OpenUV protection window data") await openuv.async_update_protection_data() async_dispatcher_send(hass, TOPIC_UPDATE) @@ -129,7 +116,7 @@ async def async_unload_entry(hass, config_entry): ) ) if unload_ok: - hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) return unload_ok @@ -139,7 +126,7 @@ async def async_migrate_entry(hass, config_entry): version = config_entry.version data = {**config_entry.data} - _LOGGER.debug("Migrating from version %s", version) + LOGGER.debug("Migrating from version %s", version) # 1 -> 2: Remove unused condition data: if version == 1: @@ -147,7 +134,7 @@ async def async_migrate_entry(hass, config_entry): data.pop(CONF_SENSORS, None) version = config_entry.version = 2 hass.config_entries.async_update_entry(config_entry, data=data) - _LOGGER.debug("Migration to version %s successful", version) + LOGGER.debug("Migration to version %s successful", version) return True @@ -166,7 +153,7 @@ class OpenUV: 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) + LOGGER.error("Error during protection data update: %s", err) self.data[DATA_PROTECTION_WINDOW] = {} async def async_update_uv_index_data(self): @@ -175,7 +162,7 @@ class OpenUV: 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) + LOGGER.error("Error during uv index data update: %s", err) self.data[DATA_UV] = {} async def async_update(self): diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 2d514b33cf3..62a83cdb141 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -1,20 +1,17 @@ """Support for OpenUV binary sensors.""" -import logging - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.util.dt import as_local, parse_datetime, utcnow -from . import ( - DATA_OPENUV_CLIENT, +from . import OpenUvEntity +from .const import ( + DATA_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN, + LOGGER, TYPE_PROTECTION_WINDOW, - OpenUvEntity, ) -_LOGGER = logging.getLogger(__name__) - ATTR_PROTECTION_WINDOW_ENDING_TIME = "end_time" ATTR_PROTECTION_WINDOW_ENDING_UV = "end_uv" ATTR_PROTECTION_WINDOW_STARTING_TIME = "start_time" @@ -25,7 +22,7 @@ BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses" async def async_setup_entry(hass, entry, async_add_entities): """Set up an OpenUV sensor based on a config entry.""" - openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] + openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] binary_sensors = [] for kind, attrs in BINARY_SENSORS.items(): @@ -86,7 +83,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): 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) + LOGGER.info("Skipping update due to missing data: %s", key) return if self._sensor_type == TYPE_PROTECTION_WINDOW: diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py index fb8641ed6ec..683e349eb50 100644 --- a/homeassistant/components/openuv/const.py +++ b/homeassistant/components/openuv/const.py @@ -1,2 +1,22 @@ """Define constants for the OpenUV component.""" +import logging + DOMAIN = "openuv" +LOGGER = logging.getLogger(__package__) + +DATA_CLIENT = "data_client" +DATA_LISTENER = "data_listener" +DATA_PROTECTION_WINDOW = "protection_window" +DATA_UV = "uv" + +TYPE_CURRENT_OZONE_LEVEL = "current_ozone_level" +TYPE_CURRENT_UV_INDEX = "current_uv_index" +TYPE_CURRENT_UV_LEVEL = "current_uv_level" +TYPE_MAX_UV_INDEX = "max_uv_index" +TYPE_PROTECTION_WINDOW = "uv_protection_window" +TYPE_SAFE_EXPOSURE_TIME_1 = "safe_exposure_time_type_1" +TYPE_SAFE_EXPOSURE_TIME_2 = "safe_exposure_time_type_2" +TYPE_SAFE_EXPOSURE_TIME_3 = "safe_exposure_time_type_3" +TYPE_SAFE_EXPOSURE_TIME_4 = "safe_exposure_time_type_4" +TYPE_SAFE_EXPOSURE_TIME_5 = "safe_exposure_time_type_5" +TYPE_SAFE_EXPOSURE_TIME_6 = "safe_exposure_time_type_6" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 781f40d75b1..b9c73023c11 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -1,12 +1,11 @@ """Support for OpenUV sensors.""" -import logging - from homeassistant.const import TIME_MINUTES, UV_INDEX from homeassistant.core import callback from homeassistant.util.dt import as_local, parse_datetime -from . import ( - DATA_OPENUV_CLIENT, +from . import OpenUvEntity +from .const import ( + DATA_CLIENT, DATA_UV, DOMAIN, TYPE_CURRENT_OZONE_LEVEL, @@ -19,11 +18,8 @@ from . import ( TYPE_SAFE_EXPOSURE_TIME_4, TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, - OpenUvEntity, ) -_LOGGER = logging.getLogger(__name__) - ATTR_MAX_UV_TIME = "time" EXPOSURE_TYPE_MAP = { @@ -80,8 +76,8 @@ SENSORS = { async def async_setup_entry(hass, entry, async_add_entities): - """Set up a Nest sensor based on a config entry.""" - openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] + """Set up a OpenUV sensor based on a config entry.""" + openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] sensors = [] for kind, attrs in SENSORS.items(): From 3ffd97acd406a692a364d3b42490f8688d234ced Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 22 Nov 2020 05:59:23 -0700 Subject: [PATCH 185/430] Fix unhandled exception when IQVIA API fails to return data (#43359) --- homeassistant/components/iqvia/__init__.py | 3 +++ homeassistant/components/iqvia/sensor.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index d730b406896..db0df3a073b 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -155,6 +155,9 @@ class IQVIAEntity(CoordinatorEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" + if not self.coordinator.last_update_success: + return + self.update_from_latest_data() self.async_write_ha_state() diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index e53ee96b1c4..48ec1cf97b1 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -105,6 +105,7 @@ class ForecastSensor(IQVIAEntity): def update_from_latest_data(self): """Update the sensor.""" data = self.coordinator.data.get("Location") + if not data or not data.get("periods"): return @@ -142,6 +143,9 @@ class IndexSensor(IQVIAEntity): @callback def update_from_latest_data(self): """Update the sensor.""" + if not self.coordinator.last_update_success: + return + try: if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): data = self.coordinator.data.get("Location") From a633341dc979e79b2f1157372e29d304d5bf5fc7 Mon Sep 17 00:00:00 2001 From: Greg Date: Sun, 22 Nov 2020 05:02:27 -0800 Subject: [PATCH 186/430] Bump envoy version to 0.17.0 (#43498) --- 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 183a53284a0..4855ee82236 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,6 +2,6 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.16.2"], + "requirements": ["envoy_reader==0.17.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b062639a2d..fce3f8eed1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ env_canada==0.2.4 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.16.2 +envoy_reader==0.17.0 # homeassistant.components.season ephem==3.7.7.0 From 62da64867c7e113bc7543b7d6e56bcc48aa4b83e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 22 Nov 2020 14:04:46 +0100 Subject: [PATCH 187/430] Optimize Sonos favorites updates (#43516) --- homeassistant/components/sonos/media_player.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index e3c24260a2e..7ec609837e4 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -947,8 +947,9 @@ class SonosEntity(MediaPlayerEntity): def update_content(self, event=None): """Update information about available content.""" - self._set_favorites() - self.schedule_update_ha_state() + if event and "favorites_update_id" in event.variables: + self._set_favorites() + self.schedule_update_ha_state() @property def volume_level(self): From 86cf184903a48c6c7e5822223f5e3e8110481e6e Mon Sep 17 00:00:00 2001 From: Lasath Fernando Date: Sun, 22 Nov 2020 05:05:15 -0800 Subject: [PATCH 188/430] Add seek support to plex media players (#43420) Turns out plexapi lib already supports this, so we just need to pass through the command. --- homeassistant/components/plex/media_player.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 94bed1db7de..5f72b912a70 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, @@ -494,6 +495,7 @@ class PlexMediaPlayer(MediaPlayerEntity): | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_STOP + | SUPPORT_SEEK | SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA @@ -557,6 +559,11 @@ class PlexMediaPlayer(MediaPlayerEntity): if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) + def media_seek(self, position): + """Send the seek command.""" + if self.device and "playback" in self._device_protocol_capabilities: + self.device.seekTo(position * 1000, self._active_media_plexapi_type) + def media_next_track(self): """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: From 0b5851e4039c4f5dcc5f66101b319521b658d18a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 22 Nov 2020 06:22:47 -0700 Subject: [PATCH 189/430] Clean up RainMachine config entry (#43508) --- .../components/rainmachine/config_flow.py | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 49eba95d047..80540491ee7 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -15,6 +15,14 @@ from .const import ( # pylint: disable=unused-import DOMAIN, ) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + } +) + class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a RainMachine config flow.""" @@ -22,24 +30,6 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - def __init__(self): - """Initialize the config flow.""" - self.data_schema = vol.Schema( - { - vol.Required(CONF_IP_ADDRESS): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - } - ) - - async def _show_form(self, errors=None): - """Show the form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=self.data_schema, - errors=errors if errors else {}, - ) - @staticmethod @callback def async_get_options_flow(config_entry): @@ -49,7 +39,9 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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() + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors={} + ) await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) self._abort_if_unique_id_configured() @@ -65,7 +57,11 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ssl=user_input.get(CONF_SSL, True), ) except RainMachineError: - return await self._show_form({CONF_PASSWORD: "invalid_auth"}) + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={CONF_PASSWORD: "invalid_auth"}, + ) # Unfortunately, RainMachine doesn't provide a way to refresh the # access token without using the IP address and password, so we have to From 498654a1e0a46a9a49076dbc842aeabbd4ca431b Mon Sep 17 00:00:00 2001 From: Daniel Rheinbay Date: Sun, 22 Nov 2020 21:41:09 +0100 Subject: [PATCH 190/430] Add vendor effects to Yeelight integration (#42711) Add effects shipped by Yeelight in their apps, as these effects are quite popular and have been missing from Home Assistant. --- homeassistant/components/yeelight/light.py | 56 +++++++++--- tests/components/yeelight/test_light.py | 101 ++++++++++++++++++++- 2 files changed, 142 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 90704a6edfb..98b7f097636 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -9,6 +9,7 @@ from yeelight import ( Flow, RGBTransition, SleepTransition, + flows, transitions as yee_transitions, ) from yeelight.enums import BulbType, LightType, PowerMode, SceneClass @@ -100,6 +101,15 @@ EFFECT_WHATSAPP = "WhatsApp" EFFECT_FACEBOOK = "Facebook" EFFECT_TWITTER = "Twitter" EFFECT_STOP = "Stop" +EFFECT_HOME = "Home" +EFFECT_NIGHT_MODE = "Night Mode" +EFFECT_DATE_NIGHT = "Date Night" +EFFECT_MOVIE = "Movie" +EFFECT_SUNRISE = "Sunrise" +EFFECT_SUNSET = "Sunset" +EFFECT_ROMANCE = "Romance" +EFFECT_HAPPY_BIRTHDAY = "Happy Birthday" +EFFECT_CANDLE_FLICKER = "Candle Flicker" YEELIGHT_TEMP_ONLY_EFFECT_LIST = [EFFECT_TEMP, EFFECT_STOP] @@ -111,6 +121,8 @@ YEELIGHT_MONO_EFFECT_LIST = [ EFFECT_WHATSAPP, EFFECT_FACEBOOK, EFFECT_TWITTER, + EFFECT_HOME, + EFFECT_CANDLE_FLICKER, *YEELIGHT_TEMP_ONLY_EFFECT_LIST, ] @@ -123,22 +135,38 @@ YEELIGHT_COLOR_EFFECT_LIST = [ EFFECT_FAST_RANDOM_LOOP, EFFECT_LSD, EFFECT_SLOWDOWN, + EFFECT_NIGHT_MODE, + EFFECT_DATE_NIGHT, + EFFECT_MOVIE, + EFFECT_SUNRISE, + EFFECT_SUNSET, + EFFECT_ROMANCE, + EFFECT_HAPPY_BIRTHDAY, *YEELIGHT_MONO_EFFECT_LIST, ] 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.random_loop, - EFFECT_LSD: yee_transitions.lsd, - EFFECT_SLOWDOWN: yee_transitions.slowdown, + EFFECT_DISCO: flows.disco, + EFFECT_TEMP: flows.temp, + EFFECT_STROBE: flows.strobe, + EFFECT_STROBE_COLOR: flows.strobe_color, + EFFECT_ALARM: flows.alarm, + EFFECT_POLICE: flows.police, + EFFECT_POLICE2: flows.police2, + EFFECT_CHRISTMAS: flows.christmas, + EFFECT_RGB: flows.rgb, + EFFECT_RANDOM_LOOP: flows.random_loop, + EFFECT_LSD: flows.lsd, + EFFECT_SLOWDOWN: flows.slowdown, + EFFECT_HOME: flows.home, + EFFECT_NIGHT_MODE: flows.night_mode, + EFFECT_DATE_NIGHT: flows.date_night, + EFFECT_MOVIE: flows.movie, + EFFECT_SUNRISE: flows.sunrise, + EFFECT_SUNSET: flows.sunset, + EFFECT_ROMANCE: flows.romance, + EFFECT_HAPPY_BIRTHDAY: flows.happy_birthday, + EFFECT_CANDLE_FLICKER: flows.candle_flicker, } VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) @@ -652,9 +680,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity): 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]()) + flow = EFFECTS_MAP[effect]() elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow(count=0, transitions=yee_transitions.random_loop(duration=250)) + flow = flows.random_loop(duration=250) elif effect == EFFECT_WHATSAPP: flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) elif effect == EFFECT_FACEBOOK: diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index e6fe16255eb..686ba6d8e82 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -13,7 +13,7 @@ from yeelight import ( TemperatureTransition, transitions, ) -from yeelight.flow import Flow +from yeelight.flow import Action, Flow from yeelight.main import _MODEL_SPECS from homeassistant.components.light import ( @@ -51,10 +51,19 @@ from homeassistant.components.yeelight import ( from homeassistant.components.yeelight.light import ( ATTR_MINUTES, ATTR_MODE, + EFFECT_CANDLE_FLICKER, + EFFECT_DATE_NIGHT, EFFECT_DISCO, EFFECT_FACEBOOK, EFFECT_FAST_RANDOM_LOOP, + EFFECT_HAPPY_BIRTHDAY, + EFFECT_HOME, + EFFECT_MOVIE, + EFFECT_NIGHT_MODE, + EFFECT_ROMANCE, EFFECT_STOP, + EFFECT_SUNRISE, + EFFECT_SUNSET, EFFECT_TWITTER, EFFECT_WHATSAPP, SERVICE_SET_AUTO_DELAY_OFF_SCENE, @@ -569,6 +578,96 @@ async def test_effects(hass: HomeAssistant): EFFECT_WHATSAPP: Flow(count=2, transitions=transitions.pulse(37, 211, 102)), EFFECT_FACEBOOK: Flow(count=2, transitions=transitions.pulse(59, 89, 152)), EFFECT_TWITTER: Flow(count=2, transitions=transitions.pulse(0, 172, 237)), + EFFECT_HOME: Flow( + count=0, + action=Action.recover, + transitions=[ + TemperatureTransition(degrees=3200, duration=500, brightness=80) + ], + ), + EFFECT_NIGHT_MODE: Flow( + count=0, + action=Action.recover, + transitions=[RGBTransition(0xFF, 0x99, 0x00, duration=500, brightness=1)], + ), + EFFECT_DATE_NIGHT: Flow( + count=0, + action=Action.recover, + transitions=[RGBTransition(0xFF, 0x66, 0x00, duration=500, brightness=50)], + ), + EFFECT_MOVIE: Flow( + count=0, + action=Action.recover, + transitions=[ + RGBTransition( + red=0x14, green=0x14, blue=0x32, duration=500, brightness=50 + ) + ], + ), + EFFECT_SUNRISE: Flow( + count=1, + action=Action.stay, + transitions=[ + RGBTransition( + red=0xFF, green=0x4D, blue=0x00, duration=50, brightness=1 + ), + TemperatureTransition(degrees=1700, duration=360000, brightness=10), + TemperatureTransition(degrees=2700, duration=540000, brightness=100), + ], + ), + EFFECT_SUNSET: Flow( + count=1, + action=Action.off, + transitions=[ + TemperatureTransition(degrees=2700, duration=50, brightness=10), + TemperatureTransition(degrees=1700, duration=180000, brightness=5), + RGBTransition( + red=0xFF, green=0x4C, blue=0x00, duration=420000, brightness=1 + ), + ], + ), + EFFECT_ROMANCE: Flow( + count=0, + action=Action.stay, + transitions=[ + RGBTransition( + red=0x59, green=0x15, blue=0x6D, duration=4000, brightness=1 + ), + RGBTransition( + red=0x66, green=0x14, blue=0x2A, duration=4000, brightness=1 + ), + ], + ), + EFFECT_HAPPY_BIRTHDAY: Flow( + count=0, + action=Action.stay, + transitions=[ + RGBTransition( + red=0xDC, green=0x50, blue=0x19, duration=1996, brightness=80 + ), + RGBTransition( + red=0xDC, green=0x78, blue=0x1E, duration=1996, brightness=80 + ), + RGBTransition( + red=0xAA, green=0x32, blue=0x14, duration=1996, brightness=80 + ), + ], + ), + EFFECT_CANDLE_FLICKER: Flow( + count=0, + action=Action.recover, + transitions=[ + TemperatureTransition(degrees=2700, duration=800, brightness=50), + TemperatureTransition(degrees=2700, duration=800, brightness=30), + TemperatureTransition(degrees=2700, duration=1200, brightness=80), + TemperatureTransition(degrees=2700, duration=800, brightness=60), + TemperatureTransition(degrees=2700, duration=1200, brightness=90), + TemperatureTransition(degrees=2700, duration=2400, brightness=50), + TemperatureTransition(degrees=2700, duration=1200, brightness=80), + TemperatureTransition(degrees=2700, duration=800, brightness=60), + TemperatureTransition(degrees=2700, duration=400, brightness=70), + ], + ), } for name, target in effects.items(): From 9ffdec63cc88ec47927ed5c0cfb26935ba713f38 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 22 Nov 2020 22:16:03 +0100 Subject: [PATCH 191/430] Optimize Sonos current playing state (#43517) --- homeassistant/components/sonos/media_player.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 7ec609837e4..874442a81c7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -724,8 +724,13 @@ class SonosEntity(MediaPlayerEntity): def update_media(self, event=None): """Update information about currently playing media.""" - transport_info = self.soco.get_current_transport_info() - new_status = transport_info.get("current_transport_state") + variables = event and event.variables + + if variables: + new_status = variables["transport_state"] + else: + transport_info = self.soco.get_current_transport_info() + new_status = transport_info["current_transport_state"] # Ignore transitions, we should get the target state soon if new_status == "TRANSITIONING": @@ -760,7 +765,6 @@ class SonosEntity(MediaPlayerEntity): self._media_title = track_info.get("title") if self.soco.is_radio_uri(track_info["uri"]): - variables = event and event.variables self.update_media_radio(variables, track_info) else: self.update_media_music(update_position, track_info) From 20db980695d7acd443892998064bb2990da89079 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Nov 2020 22:39:07 +0100 Subject: [PATCH 192/430] Upgrade Docker base image to 2020.11.1 (#43538) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 43e23d32094..31c6085480c 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2020.10.1", - "armhf": "homeassistant/armhf-homeassistant-base:2020.10.1", - "armv7": "homeassistant/armv7-homeassistant-base:2020.10.1", - "amd64": "homeassistant/amd64-homeassistant-base:2020.10.1", - "i386": "homeassistant/i386-homeassistant-base:2020.10.1" + "aarch64": "homeassistant/aarch64-homeassistant-base:2020.11.1", + "armhf": "homeassistant/armhf-homeassistant-base:2020.11.1", + "armv7": "homeassistant/armv7-homeassistant-base:2020.11.1", + "amd64": "homeassistant/amd64-homeassistant-base:2020.11.1", + "i386": "homeassistant/i386-homeassistant-base:2020.11.1" }, "labels": { "io.hass.type": "core" From 885e393df9ec9765822ca96d262076d44ef382a5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 22 Nov 2020 22:42:16 +0100 Subject: [PATCH 193/430] Make Brother uptime sensor disabled by default (#43478) --- homeassistant/components/brother/const.py | 30 ++++++++++++++++- homeassistant/components/brother/sensor.py | 3 +- tests/components/brother/__init__.py | 19 ++++++----- tests/components/brother/test_sensor.py | 38 +++++++++++++++++++--- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 9aa0a4f4a00..cbb3d2a70cb 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -18,6 +18,7 @@ ATTR_DRUM_COUNTER = "drum_counter" ATTR_DRUM_REMAINING_LIFE = "drum_remaining_life" ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages" ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter" +ATTR_ENABLED = "enabled" ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life" ATTR_ICON = "icon" ATTR_LABEL = "label" @@ -51,71 +52,85 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:printer", ATTR_LABEL: ATTR_STATUS.title(), ATTR_UNIT: None, + ATTR_ENABLED: True, }, ATTR_PAGE_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_BW_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_COLOR_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_DUPLEX_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, + ATTR_ENABLED: True, }, ATTR_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_CYAN_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_MAGENTA_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_YELLOW_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BELT_UNIT_REMAINING_LIFE: { ATTR_ICON: "mdi:current-ac", ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_FUSER_REMAINING_LIFE: { ATTR_ICON: "mdi:water-outline", ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_LASER_REMAINING_LIFE: { ATTR_ICON: "mdi:spotlight-beam", ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_PF_KIT_1_REMAINING_LIFE: { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_PF_KIT_MP_REMAINING_LIFE: { ATTR_ICON: "mdi:printer-3d", @@ -126,41 +141,54 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_CYAN_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_MAGENTA_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_YELLOW_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_CYAN_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_MAGENTA_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_YELLOW_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, + }, + ATTR_UPTIME: { + ATTR_ICON: None, + ATTR_LABEL: ATTR_UPTIME.title(), + ATTR_UNIT: None, + ATTR_ENABLED: False, }, - ATTR_UPTIME: {ATTR_ICON: None, ATTR_LABEL: ATTR_UPTIME.title(), ATTR_UNIT: None}, } diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 7239976f85e..97890e83cff 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -15,6 +15,7 @@ from .const import ( ATTR_DRUM_COUNTER, ATTR_DRUM_REMAINING_LIFE, ATTR_DRUM_REMAINING_PAGES, + ATTR_ENABLED, ATTR_ICON, ATTR_LABEL, ATTR_MAGENTA_DRUM_COUNTER, @@ -139,4 +140,4 @@ class BrotherPrinterSensor(CoordinatorEntity): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - return True + return SENSOR_TYPES[self.kind][ATTR_ENABLED] diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index 1a3ba2a3e20..b24ef97705b 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -8,7 +8,7 @@ from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass, skip_setup=False) -> MockConfigEntry: """Set up the Brother integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -16,12 +16,15 @@ async def init_integration(hass) -> MockConfigEntry: unique_id="0123456789", data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, ) - with patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + + entry.add_to_hass(hass) + + if not skip_setup: + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index c8dc91ebcf4..ac32e1b983c 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -2,7 +2,8 @@ from datetime import datetime, timedelta import json -from homeassistant.components.brother.const import UNIT_PAGES +from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -25,13 +26,27 @@ ATTR_COUNTER = "counter" async def test_sensors(hass): """Test states of the sensors.""" + entry = await init_integration(hass, skip_setup=True) + + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456789_uptime", + suggested_object_id="hl_l2340dw_uptime", + disabled_by=None, + ) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) with patch( "homeassistant.components.brother.sensor.utcnow", return_value=test_time + ), patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), ): - await init_integration(hass) - - registry = await hass.helpers.entity_registry.async_get_registry() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("sensor.hl_l2340dw_status") assert state @@ -224,6 +239,21 @@ async def test_sensors(hass): assert entry.unique_id == "0123456789_uptime" +async def test_disabled_by_default_sensors(hass): + """Test the disabled by default Brother sensors.""" + await init_integration(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + state = hass.states.get("sensor.hl_l2340dw_uptime") + assert state is None + + entry = registry.async_get("sensor.hl_l2340dw_uptime") + assert entry + assert entry.unique_id == "0123456789_uptime" + assert entry.disabled + assert entry.disabled_by == "integration" + + async def test_availability(hass): """Ensure that we mark the entities unavailable correctly when device is offline.""" await init_integration(hass) From 492ef81069f3a7f13bb3bef760396704e5410c68 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 23 Nov 2020 00:04:13 +0000 Subject: [PATCH 194/430] [ci skip] Translation update --- .../components/binary_sensor/translations/ru.json | 4 ++++ homeassistant/components/kodi/translations/ca.json | 1 + homeassistant/components/kodi/translations/et.json | 1 + homeassistant/components/kodi/translations/pl.json | 1 + homeassistant/components/kodi/translations/ru.json | 1 + homeassistant/components/kodi/translations/zh-Hant.json | 1 + homeassistant/components/local_ip/translations/ca.json | 1 + homeassistant/components/ovo_energy/translations/ca.json | 8 ++++++++ homeassistant/components/ovo_energy/translations/no.json | 5 +++++ homeassistant/components/ovo_energy/translations/pl.json | 8 ++++++++ .../components/ovo_energy/translations/zh-Hant.json | 8 ++++++++ 11 files changed, 39 insertions(+) diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 8a192c3dbe0..fe9e6773547 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -142,6 +142,10 @@ "off": "\u041d\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f ", "on": "\u0414\u0432\u0438\u0436\u0435\u043d\u0438\u0435" }, + "moving": { + "off": "\u041d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "on": "\u041f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f" + }, "occupancy": { "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e", "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e" diff --git a/homeassistant/components/kodi/translations/ca.json b/homeassistant/components/kodi/translations/ca.json index 8dd03fe9fff..fbeaa3d4c71 100644 --- a/homeassistant/components/kodi/translations/ca.json +++ b/homeassistant/components/kodi/translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_uuid": "La inst\u00e0ncia de Kodi no t\u00e9 un identificador \u00fanic. Probablement, aix\u00f2 es deu a una versi\u00f3 antiga de Kodi (17.x o inferior). Pots configurar la integraci\u00f3 manualment o actualitzar Kodi a una versi\u00f3 m\u00e9s recent.", "unknown": "Error inesperat" }, "error": { diff --git a/homeassistant/components/kodi/translations/et.json b/homeassistant/components/kodi/translations/et.json index a569c470e1d..f12665dfe8a 100644 --- a/homeassistant/components/kodi/translations/et.json +++ b/homeassistant/components/kodi/translations/et.json @@ -4,6 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", + "no_uuid": "Kodi eksemplaril puudub ID. See on k\u00f5ige t\u00f5en\u00e4olisemalt tingitud vananenud Kodi versiooni t\u00f5ttu (17.x v\u00f5i vanem). Sidumist saad seadistada k\u00e4sitsi v\u00f5i t\u00e4ienda Kodi uuemale versioonile.", "unknown": "Tundmatu viga" }, "error": { diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index 78fdf95a7a8..a1f0a3a6eed 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -4,6 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_uuid": "Kodi nie ma unikalnego identyfikatora. Najprawdopodobniej jest to spowodowane star\u0105 wersj\u0105 Kodi (17.x lub starsz\u0105). Mo\u017cesz skonfigurowa\u0107 integracj\u0119 r\u0119cznie lub zaktualizowa\u0107 Kodi do nowszej wersji.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index 8516d609de1..312008c9b62 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -4,6 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "no_uuid": "\u0423 \u044d\u0442\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u044d\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u0442\u0430\u0440\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439 Kodi (17.x \u0438\u043b\u0438 \u043d\u0438\u0436\u0435). \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e Kodi.", "unknown": "\u041d\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/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 3e9065c140f..a4aaf909344 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -4,6 +4,7 @@ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_uuid": "Kodi \u5be6\u4f8b\u6c92\u6709\u552f\u4e00 ID\u3002\u901a\u5e38\u662f\u56e0\u70ba Kodi \u7248\u672c\u904e\u820a\uff08\u4f4e\u65bc 17.x\uff09\u3002\u53ef\u4ee5\u624b\u52d5\u8a2d\u5b9a\u6574\u5408\u6216\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Kodi\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/local_ip/translations/ca.json b/homeassistant/components/local_ip/translations/ca.json index 7be2a2d70d6..cfd425e3034 100644 --- a/homeassistant/components/local_ip/translations/ca.json +++ b/homeassistant/components/local_ip/translations/ca.json @@ -8,6 +8,7 @@ "data": { "name": "Nom del sensor" }, + "description": "Vols comen\u00e7ar la configuraci\u00f3?", "title": "Adre\u00e7a IP local" } } diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index 4f8525bfd85..3cc971434a4 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -5,7 +5,15 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Contrasenya" + }, + "description": "L'autenticaci\u00f3 d'OVO Energy ha fallat. Introdueix les teves credencials actuals.", + "title": "Reautenticaci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 72b69e43b9b..99cc17cc348 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -6,6 +6,11 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { + "reauth": { + "data": { + "password": "Passord" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index 541df85dc28..5767f3f7cf2 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -5,7 +5,15 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Has\u0142o" + }, + "description": "Uwierzytelnianie dla OVO Energy nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.", + "title": "Ponowne uwierzytelnianie" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json index a6ba2f8b624..f557a83009c 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hant.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -5,7 +5,15 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, + "flow_title": "OVO Energy\uff1a{username}", "step": { + "reauth": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "OVO Energy \u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u7684\u6191\u8b49\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49" + }, "user": { "data": { "password": "\u5bc6\u78bc", From a4f7b7d78441d97d1c58b96f9ef94dacbab0e74e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 22 Nov 2020 20:15:38 -0700 Subject: [PATCH 195/430] Fix bug related to possibly missing task ID in Notion API data (#43330) * Fix bug related to possibly missing task ID in Notion API data * Calculate unique ID once * Code review * Simplify * Code review --- homeassistant/components/notion/__init__.py | 16 ++++++++++------ homeassistant/components/notion/binary_sensor.py | 4 ++-- homeassistant/components/notion/sensor.py | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index be4a47c6d85..561f3edf896 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -160,14 +160,17 @@ class NotionEntity(CoordinatorEntity): self._sensor_id = sensor_id self._state = None self._system_id = system_id - self._task_id = task_id + self._unique_id = ( + f'{sensor_id}_{self.coordinator.data["tasks"][task_id]["task_type"]}' + ) + self.task_id = task_id @property def available(self) -> bool: """Return True if entity is available.""" return ( self.coordinator.last_update_success - and self._task_id in self.coordinator.data["tasks"] + and self.task_id in self.coordinator.data["tasks"] ) @property @@ -204,8 +207,7 @@ class NotionEntity(CoordinatorEntity): @property def unique_id(self) -> str: """Return a unique, unchanging string that represents this entity.""" - task = self.coordinator.data["tasks"][self._task_id] - return f'{self._sensor_id}_{task["task_type"]}' + return self._unique_id async def _async_update_bridge_id(self) -> None: """Update the entity's bridge ID if it has changed. @@ -246,8 +248,10 @@ class NotionEntity(CoordinatorEntity): @callback def _handle_coordinator_update(self): """Respond to a DataUpdateCoordinator update.""" - self.hass.async_create_task(self._async_update_bridge_id()) - self._async_update_from_latest_data() + if self.task_id in self.coordinator.data["tasks"]: + self.hass.async_create_task(self._async_update_bridge_id()) + self._async_update_from_latest_data() + self.async_write_ha_state() async def async_added_to_hass(self): diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index b8fd96fabc5..a198903b99a 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -77,7 +77,7 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self.coordinator.data["tasks"][self._task_id] + task = self.coordinator.data["tasks"][self.task_id] if "value" in task["status"]: self._state = task["status"]["value"] @@ -87,7 +87,7 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return whether the sensor is on or off.""" - task = self.coordinator.data["tasks"][self._task_id] + task = self.coordinator.data["tasks"][self.task_id] if task["task_type"] == SENSOR_BATTERY: return self._state == "critical" diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 99af00c3b1a..978e0aac46a 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -76,7 +76,7 @@ class NotionSensor(NotionEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self.coordinator.data["tasks"][self._task_id] + task = self.coordinator.data["tasks"][self.task_id] if task["task_type"] == SENSOR_TEMPERATURE: self._state = round(float(task["status"]["value"]), 1) From ffe0c1cd58c92ebf42bf48c8e88df120aa5fd6bb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 23 Nov 2020 09:22:44 +0100 Subject: [PATCH 196/430] Add device id to deconz_event (#43552) --- homeassistant/components/deconz/deconz_event.py | 5 ++++- tests/components/deconz/test_deconz_event.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 968ab3cee39..7ba372bf685 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,7 +1,7 @@ """Representation of a deCONZ remote.""" from pydeconz.sensor import Switch -from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID +from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify @@ -94,6 +94,9 @@ class DeconzEvent(DeconzBase): CONF_EVENT: self._device.state, } + if self.device_id: + data[CONF_DEVICE_ID] = self.device_id + if self._device.gesture is not None: data[CONF_GESTURE] = self._device.gesture diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index e1492fb0fcf..14faf1a938c 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -77,6 +77,7 @@ async def test_deconz_events(hass): "id": "switch_1", "unique_id": "00:00:00:00:00:00:00:01", "event": 2000, + "device_id": gateway.events[0].device_id, } gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}}) @@ -88,6 +89,7 @@ async def test_deconz_events(hass): "unique_id": "00:00:00:00:00:00:00:03", "event": 2000, "gesture": 1, + "device_id": gateway.events[2].device_id, } gateway.api.sensors["4"].update({"state": {"gesture": 0}}) @@ -99,6 +101,7 @@ async def test_deconz_events(hass): "unique_id": "00:00:00:00:00:00:00:04", "event": 1000, "gesture": 0, + "device_id": gateway.events[3].device_id, } gateway.api.sensors["5"].update( @@ -113,6 +116,7 @@ async def test_deconz_events(hass): "event": 6002, "angle": 110, "xy": [0.5982, 0.3897], + "device_id": gateway.events[4].device_id, } await hass.config_entries.async_unload(config_entry.entry_id) From b144a980daac1c41873ec1a8a2607b1a95c6efbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 09:36:52 +0100 Subject: [PATCH 197/430] Bump actions/stale from v3.0.13 to v3.0.14 (#43551) Bumps [actions/stale](https://github.com/actions/stale) from v3.0.13 to v3.0.14. - [Release notes](https://github.com/actions/stale/releases) - [Commits](https://github.com/actions/stale/compare/v3.0.13...87c2b794b9b47a9bec68ae03c01aeb572ffebdb1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1fe635ac57f..519353c81a9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: # The 90 day stale policy # Used for: Everything (unless 30 day policy below beats it) - name: 90 days stale policy - uses: actions/stale@v3.0.13 + uses: actions/stale@v3.0.14 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -48,7 +48,7 @@ jobs: # - Issues that are pending more information (incomplete issues) # - PRs that are not marked as new-integration - name: 30 days stale policy - uses: actions/stale@v3.0.13 + uses: actions/stale@v3.0.14 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # PRs have a CLA signed label, we can misuse it to apply this policy From 8ab15f686791833ba6f313b85a188524fb6b313d Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 23 Nov 2020 03:42:28 -0500 Subject: [PATCH 198/430] Update Blinkpy to fix non-updating blink mini cameras (#43549) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index ca3f1f6efee..17d737bcaf3 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.16.3"], + "requirements": ["blinkpy==0.16.4"], "codeowners": ["@fronzbot"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index fce3f8eed1e..0021da01332 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -351,7 +351,7 @@ bizkaibus==0.1.1 blebox_uniapi==1.3.2 # homeassistant.components.blink -blinkpy==0.16.3 +blinkpy==0.16.4 # homeassistant.components.blinksticklight blinkstick==1.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d647c54678..0ecd14a10b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -195,7 +195,7 @@ bellows==0.20.3 blebox_uniapi==1.3.2 # homeassistant.components.blink -blinkpy==0.16.3 +blinkpy==0.16.4 # homeassistant.components.bond bond-api==0.1.8 From 47ff04d9adcdf0e3509d76820f567f87e7df8bfb Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 23 Nov 2020 10:20:06 +0100 Subject: [PATCH 199/430] Optimize reading of Sonos source mode (#43541) --- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/media_player.py | 17 +++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 49826ebc410..66e6587b9ff 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.36"], + "requirements": ["pysonos==0.0.37"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 874442a81c7..48b22256030 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -9,7 +9,13 @@ import urllib.parse import async_timeout import pysonos from pysonos import alarms -from pysonos.core import PLAY_MODE_BY_MEANING, PLAY_MODES +from pysonos.core import ( + PLAY_MODE_BY_MEANING, + PLAY_MODES, + PLAYING_LINE_IN, + PLAYING_RADIO, + PLAYING_TV, +) from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.music_library import pysonos.snapshot @@ -750,9 +756,12 @@ class SonosEntity(MediaPlayerEntity): update_position = new_status != self._status self._status = new_status - if self.soco.is_playing_tv: + track_uri = variables["current_track_uri"] if variables else None + whats_playing = self.soco.whats_playing(track_uri) + + if whats_playing == PLAYING_TV: self.update_media_linein(SOURCE_TV) - elif self.soco.is_playing_line_in: + elif whats_playing == PLAYING_LINE_IN: self.update_media_linein(SOURCE_LINEIN) else: track_info = self.soco.get_current_track_info() @@ -764,7 +773,7 @@ class SonosEntity(MediaPlayerEntity): self._media_album_name = track_info.get("album") self._media_title = track_info.get("title") - if self.soco.is_radio_uri(track_info["uri"]): + if whats_playing == PLAYING_RADIO: self.update_media_radio(variables, track_info) else: self.update_media_music(update_position, track_info) diff --git a/requirements_all.txt b/requirements_all.txt index 0021da01332..55f67ce2e92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1695,7 +1695,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.36 +pysonos==0.0.37 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ecd14a10b1..bcea75282fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -845,7 +845,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.36 +pysonos==0.0.37 # homeassistant.components.spc pyspcwebgw==0.4.0 From 3b105c415bb0d03f94a78a568674ee6f9c5e264b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 23 Nov 2020 10:26:50 +0100 Subject: [PATCH 200/430] Mill Heater: Add support for Energy consumption (#43523) --- homeassistant/components/mill/climate.py | 2 ++ homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 67ed692691b..0bb94242d64 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -114,6 +114,8 @@ class MillHeater(ClimateEntity): "heating": self._heater.is_heating, "controlled_by_tibber": self._heater.tibber_control, "heater_generation": 1 if self._heater.is_gen1 else 2, + "consumption_today": self._heater.day_consumption, + "consumption_total": self._heater.total_consumption, } if self._heater.room: res["room"] = self._heater.room.name diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 684be0479bd..d0faa1e2ed5 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.3.4"], + "requirements": ["millheater==0.4.0"], "codeowners": ["@danielhiversen"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 55f67ce2e92..918a0bd1122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -943,7 +943,7 @@ mficlient==0.3.0 miflora==0.7.0 # homeassistant.components.mill -millheater==0.3.4 +millheater==0.4.0 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcea75282fa..d3ef6b7a0bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,7 +468,7 @@ meteofrance-api==0.1.1 mficlient==0.3.0 # homeassistant.components.mill -millheater==0.3.4 +millheater==0.4.0 # homeassistant.components.minio minio==4.0.9 From 55cbd5aa0d12bfb2a4c8ea9640f2cdd5e7986db6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 23 Nov 2020 11:37:11 +0100 Subject: [PATCH 201/430] Track deCONZ lib changes to light based devices (#43366) * Improve control of covers * Log backtrace if available * Do not create entity for controller tool Binary sensor should use state rather than is_tripped Add some more tests to lights and sensors * Bump dependency to v74 * Fix Balloobs comments --- .../components/deconz/binary_sensor.py | 2 +- homeassistant/components/deconz/cover.py | 27 ++---- homeassistant/components/deconz/fan.py | 9 +- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/deconz/light.py | 6 +- homeassistant/components/deconz/lock.py | 13 +-- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/switch.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_binary_sensor.py | 2 +- tests/components/deconz/test_cover.py | 82 ++++++++++++++++++- tests/components/deconz/test_light.py | 23 ++++++ tests/components/deconz/test_sensor.py | 78 ++++++++++++++++++ 14 files changed, 206 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 5b536aeb74c..184bce8defc 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -84,7 +84,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): @property def is_on(self): """Return true if sensor is on.""" - return self._device.is_tripped + return self._device.state @property def device_class(self): diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 5982aead14f..ab5f6e0be9e 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -18,10 +18,7 @@ from .gateway import get_gateway_from_config_entry async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up covers for deCONZ component. - - Covers are based on the same device class as lights in deCONZ. - """ + """Set up covers for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @@ -66,12 +63,12 @@ class DeconzCover(DeconzDevice, CoverEntity): @property def current_cover_position(self): """Return the current position of the cover.""" - return 100 - int(self._device.brightness / 254 * 100) + return 100 - self._device.position @property def is_closed(self): """Return if the cover is closed.""" - return self._device.state + return not self._device.is_open @property def device_class(self): @@ -88,26 +85,16 @@ class DeconzCover(DeconzDevice, CoverEntity): 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"] = 254 - int(position / 100 * 254) - - await self._device.async_set_state(data) + await self._device.set_position(kwargs[ATTR_POSITION]) async def async_open_cover(self, **kwargs): """Open cover.""" - data = {ATTR_POSITION: 100} - await self.async_set_cover_position(**data) + await self._device.open() async def async_close_cover(self, **kwargs): """Close cover.""" - data = {ATTR_POSITION: 0} - await self.async_set_cover_position(**data) + await self._device.close() async def async_stop_cover(self, **kwargs): """Stop cover.""" - data = {"bri_inc": 0} - await self._device.async_set_state(data) + await self._device.stop() diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 7936a8fead5..69e77befb4f 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -32,10 +32,7 @@ def convert_speed(speed: int) -> str: async def async_setup_entry(hass, config_entry, async_add_entities) -> None: - """Set up fans for deCONZ component. - - Fans are based on the same device class as lights in deCONZ. - """ + """Set up fans for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @@ -108,9 +105,7 @@ class DeconzFan(DeconzDevice, FanEntity): if speed not in SPEEDS: raise ValueError(f"Unsupported speed {speed}") - data = {"speed": SPEEDS[speed]} - - await self._device.async_set_state(data) + await self._device.set_speed(SPEEDS[speed]) async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on fan.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 475b3c48525..8a9daded289 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -171,7 +171,7 @@ class DeconzGateway: raise ConfigEntryNotReady from err except Exception as err: # pylint: disable=broad-except - LOGGER.error("Error connecting with deCONZ gateway: %s", err) + LOGGER.error("Error connecting with deCONZ gateway: %s", err, exc_info=True) return False for component in SUPPORTED_PLATFORMS: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 3bdf2c67caa..01e36e2ccf7 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -34,12 +34,16 @@ from .const import ( from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry +CONTROLLER = ["Configuration tool"] + 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) gateway.entities[DOMAIN] = set() + other_light_resource_types = CONTROLLER + COVER_TYPES + LOCK_TYPES + SWITCH_TYPES + @callback def async_add_light(lights): """Add light from deCONZ.""" @@ -47,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for light in lights: if ( - light.type not in COVER_TYPES + LOCK_TYPES + SWITCH_TYPES + light.type not in other_light_resource_types and light.uniqueid not in gateway.entities[DOMAIN] ): entities.append(DeconzLight(light, gateway)) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 175d422ea1b..a5b53e86af5 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -9,10 +9,7 @@ from .gateway import get_gateway_from_config_entry async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up locks for deCONZ component. - - Locks are based on the same device class as lights in deCONZ. - """ + """Set up locks for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @@ -46,14 +43,12 @@ class DeconzLock(DeconzDevice, LockEntity): @property def is_locked(self): """Return true if lock is on.""" - return self._device.state + return self._device.is_locked async def async_lock(self, **kwargs): """Lock the lock.""" - data = {"on": True} - await self._device.async_set_state(data) + await self._device.lock() async def async_unlock(self, **kwargs): """Unlock the lock.""" - data = {"on": False} - await self._device.async_set_state(data) + await self._device.unlock() diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 6a47864375e..7b8abe82472 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==73"], + "requirements": ["pydeconz==74"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 509282e45f2..84bb0f84da1 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -75,14 +75,12 @@ class DeconzSiren(DeconzDevice, SwitchEntity): @property def is_on(self): """Return true if switch is on.""" - return self._device.alert == "lselect" + return self._device.is_on async def async_turn_on(self, **kwargs): """Turn on switch.""" - data = {"alert": "lselect"} - await self._device.async_set_state(data) + await self._device.turn_on() async def async_turn_off(self, **kwargs): """Turn off switch.""" - data = {"alert": "none"} - await self._device.async_set_state(data) + await self._device.turn_off() diff --git a/requirements_all.txt b/requirements_all.txt index 918a0bd1122..993806e2a40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1334,7 +1334,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==73 +pydeconz==74 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3ef6b7a0bc..0ec6690d1f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==73 +pydeconz==74 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 5872aee1bf1..5038c5bf3f2 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -40,7 +40,7 @@ SENSORS = { "id": "CLIP presence sensor id", "name": "CLIP presence sensor", "type": "CLIPPresence", - "state": {}, + "state": {"presence": False}, "config": {}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 485ae4239be..3e31006438a 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -3,9 +3,11 @@ from copy import deepcopy from homeassistant.components.cover import ( + ATTR_POSITION, DOMAIN as COVER_DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, ) from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN @@ -30,7 +32,7 @@ COVERS = { "id": "Window covering device id", "name": "Window covering device", "type": "Window covering device", - "state": {"bri": 254, "on": True, "reachable": True}, + "state": {"lift": 100, "open": False, "reachable": True}, "modelid": "lumi.curtain", "uniqueid": "00:00:00:00:00:00:00:01-00", }, @@ -105,7 +107,67 @@ async def test_cover(hass): assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED - # Verify service calls + # Verify service calls for cover + + windows_covering_device = gateway.api.lights["2"] + + # Service open cover + + with patch.object( + windows_covering_device, "_request", return_value=True + ) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.window_covering_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/2/state", json={"open": True}) + + # Service close cover + + with patch.object( + windows_covering_device, "_request", return_value=True + ) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.window_covering_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/2/state", json={"open": False}) + + # Service set cover position + + with patch.object( + windows_covering_device, "_request", return_value=True + ) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 50}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 50}) + + # Service stop cover movement + + with patch.object( + windows_covering_device, "_request", return_value=True + ) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.window_covering_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/2/state", json={"bri_inc": 0}) + + # Verify service calls for legacy cover level_controllable_cover_device = gateway.api.lights["1"] @@ -135,9 +197,21 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/lights/1/state", json={"on": True, "bri": 254} + set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) + + # Service set cover position + + with patch.object( + level_controllable_cover_device, "_request", return_value=True + ) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 50}, + blocking=True, ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 127}) # Service stop cover movement diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 460f81e830c..b971de28d43 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -330,3 +330,26 @@ async def test_disable_light_groups(hass): assert len(hass.states.async_all()) == 5 assert hass.states.get("light.light_group") is None + + +async def test_configuration_tool(hass): + """Test that lights or groups entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = { + "0": { + "etag": "26839cb118f5bf7ba1f2108256644010", + "hascolor": False, + "lastannounced": None, + "lastseen": "2020-11-22T11:27Z", + "manufacturername": "dresden elektronik", + "modelid": "ConBee II", + "name": "Configuration tool 1", + "state": {"reachable": True}, + "swversion": "0x264a0700", + "type": "Configuration tool", + "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", + } + } + await setup_deconz_integration(hass, get_state_response=data) + + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 8b2f1e4da76..def2a1412e5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -242,3 +242,81 @@ async def test_add_battery_later(hass): assert len(remote._callbacks) == 2 # Event and battery entity assert hass.states.get("sensor.switch_1_battery_level") + + +async def test_air_quality_sensor(hass): + """Test successful creation of air quality sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": {"on": True, "reachable": True}, + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "Air quality", + "state": { + "airquality": "poor", + "airqualityppb": 809, + "lastupdated": "2020-11-20T22:48:00.209", + }, + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", + } + } + await setup_deconz_integration(hass, get_state_response=data) + + assert len(hass.states.async_all()) == 1 + + air_quality = hass.states.get("sensor.air_quality") + assert air_quality.state == "poor" + + +async def test_time_sensor(hass): + """Test successful creation of time sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": {"battery": 40, "on": True, "reachable": True}, + "ep": 1, + "etag": "28e796678d9a24712feef59294343bb6", + "lastseen": "2020-11-22T11:26Z", + "manufacturername": "Danfoss", + "modelid": "eTRV0100", + "name": "Time", + "state": { + "lastset": "2020-11-19T08:07:08Z", + "lastupdated": "2020-11-22T10:51:03.444", + "localtime": "2020-11-22T10:51:01", + "utc": "2020-11-22T10:51:01Z", + }, + "swversion": "20200429", + "type": "ZHATime", + "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", + } + } + await setup_deconz_integration(hass, get_state_response=data) + + assert len(hass.states.async_all()) == 2 + + time = hass.states.get("sensor.time") + assert time.state == "2020-11-19T08:07:08Z" + + time_battery = hass.states.get("sensor.time_battery_level") + assert time_battery.state == "40" + + +async def test_unsupported_sensor(hass): + """Test that unsupported sensors doesn't break anything.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": {"type": "not supported", "name": "name", "state": {}, "config": {}} + } + await setup_deconz_integration(hass, get_state_response=data) + + assert len(hass.states.async_all()) == 1 + + unsupported_sensor = hass.states.get("sensor.name") + assert unsupported_sensor.state == "unknown" From a3d9a9433927b6c5d7269d8e5c1f56e378a79da6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Nov 2020 13:10:30 +0100 Subject: [PATCH 202/430] Upgrade Docker base image to 2020.11.2 (#43560) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 31c6085480c..49cee1ff280 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2020.11.1", - "armhf": "homeassistant/armhf-homeassistant-base:2020.11.1", - "armv7": "homeassistant/armv7-homeassistant-base:2020.11.1", - "amd64": "homeassistant/amd64-homeassistant-base:2020.11.1", - "i386": "homeassistant/i386-homeassistant-base:2020.11.1" + "aarch64": "homeassistant/aarch64-homeassistant-base:2020.11.2", + "armhf": "homeassistant/armhf-homeassistant-base:2020.11.2", + "armv7": "homeassistant/armv7-homeassistant-base:2020.11.2", + "amd64": "homeassistant/amd64-homeassistant-base:2020.11.2", + "i386": "homeassistant/i386-homeassistant-base:2020.11.2" }, "labels": { "io.hass.type": "core" From 2fcd1a250b1635a22efe245940c3758fe72475b6 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Tue, 24 Nov 2020 00:05:22 +1100 Subject: [PATCH 203/430] update solax to 0.2.5 (#43564) --- homeassistant/components/solax/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index bf2d3d72cc5..232715ebe18 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -2,6 +2,6 @@ "domain": "solax", "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": ["solax==0.2.4"], + "requirements": ["solax==0.2.5"], "codeowners": ["@squishykid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 993806e2a40..0bcb6285a21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ solaredge-local==0.2.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.4 +solax==0.2.5 # homeassistant.components.honeywell somecomfort==0.5.2 From 9368ab5480b9f59271c31a65066f2d753cc3877e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Nov 2020 14:09:17 +0100 Subject: [PATCH 204/430] Upgrade sentry-sdk to 0.19.4 (#43504) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index c5c75d7f3fe..be07586cebd 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.19.3"], + "requirements": ["sentry-sdk==0.19.4"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0bcb6285a21..bffefa9e408 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2000,7 +2000,7 @@ sense-hat==2.2.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.3 +sentry-sdk==0.19.4 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ec6690d1f5..3a76e8992c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -966,7 +966,7 @@ samsungtvws==1.4.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.3 +sentry-sdk==0.19.4 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 0c64873c108da4c1a4fbe5e37d6939d771ba59df Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 23 Nov 2020 17:54:06 +0100 Subject: [PATCH 205/430] Update denonavr to 0.9.7 (#43546) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 7a00779dcca..c8341a3ec2c 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.6", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.7", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index bffefa9e408..2990802b2f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -481,7 +481,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.6 +denonavr==0.9.7 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a76e8992c0..006c2c78de6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.6 +denonavr==0.9.7 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 From b272e16b4450d296afca3f549b051361f545ecab Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 23 Nov 2020 19:13:52 +0100 Subject: [PATCH 206/430] Fix RTS cover with set position available (#43555) --- homeassistant/components/somfy/__init__.py | 13 ++++-- homeassistant/components/somfy/cover.py | 52 +++++++++++++++++----- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 99b0a2ee564..728e54b456f 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -164,12 +164,12 @@ class SomfyEntity(CoordinatorEntity, Entity): return self.coordinator.data[self._id] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id base on the id returned by Somfy.""" return self._id @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self.device.name @@ -188,13 +188,18 @@ class SomfyEntity(CoordinatorEntity, Entity): "manufacturer": "Somfy", } - def has_capability(self, capability): + def has_capability(self, capability: str) -> bool: """Test if device has a capability.""" capabilities = self.device.capabilities return bool([c for c in capabilities if c.name == capability]) + def has_state(self, state: str) -> bool: + """Test if device has a state.""" + states = self.device.states + return bool([c for c in states if c.name == state]) + @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return if the device has an assumed state.""" return not bool(self.device.states) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 605d58a941b..696412ac3c7 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -8,6 +8,14 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, DEVICE_CLASS_BLIND, DEVICE_CLASS_SHUTTER, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_OPEN @@ -57,10 +65,32 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self.cover = None self._create_device() - def _create_device(self): + def _create_device(self) -> Blind: """Update the device with the latest data.""" self.cover = Blind(self.device, self.api) + @property + def supported_features(self) -> int: + """Flag supported features.""" + supported_features = 0 + if self.has_capability("open"): + supported_features |= SUPPORT_OPEN + if self.has_capability("close"): + supported_features |= SUPPORT_CLOSE + if self.has_capability("stop"): + supported_features |= SUPPORT_STOP + if self.has_capability("position"): + supported_features |= SUPPORT_SET_POSITION + if self.has_capability("rotation"): + supported_features |= ( + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION + ) + + return supported_features + async def async_close_cover(self, **kwargs): """Close the cover.""" self._is_closing = True @@ -105,10 +135,9 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): @property def current_cover_position(self): """Return the current position of cover shutter.""" - position = None - if self.has_capability("position"): - position = 100 - self.cover.get_position() - return position + if not self.has_state("position"): + return None + return 100 - self.cover.get_position() @property def is_opening(self): @@ -125,25 +154,24 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): return self._is_closing @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" is_closed = None - if self.has_capability("position"): + if self.has_state("position"): is_closed = self.cover.is_closed() elif self.optimistic: is_closed = self._closed return is_closed @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int: """Return current position of cover tilt. None is unknown, 0 is closed, 100 is fully open. """ - orientation = None - if self.has_capability("rotation"): - orientation = 100 - self.cover.orientation - return orientation + if not self.has_state("orientation"): + return None + return 100 - self.cover.orientation def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" From 65796eee2d20fbe423bffd547048134a4e7a39f7 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 23 Nov 2020 10:58:30 -0800 Subject: [PATCH 207/430] Add codeowner to Enphase Envoy manifest (#43402) --- CODEOWNERS | 1 + homeassistant/components/enphase_envoy/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 29740431c37..a1de0a34518 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -132,6 +132,7 @@ homeassistant/components/emoncms/* @borpin homeassistant/components/emulated_kasa/* @kbickar homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer +homeassistant/components/enphase_envoy/* @gtdiehl homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @michaeldavie homeassistant/components/ephember/* @ttroy50 diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 4855ee82236..e6ab8dbf6a9 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,5 +3,7 @@ "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": ["envoy_reader==0.17.0"], - "codeowners": [] + "codeowners": [ + "@gtdiehl" + ] } From 612e74703b57756d4d65a8e8333522358cb94ea5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Nov 2020 20:49:13 +0100 Subject: [PATCH 208/430] Add webhook to default config (#43521) --- .../components/default_config/manifest.json | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 8c6a3dde6cf..9a533092b8b 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -5,8 +5,14 @@ "dependencies": [ "automation", "cloud", + "counter", "frontend", "history", + "input_boolean", + "input_datetime", + "input_number", + "input_select", + "input_text", "logbook", "map", "media_source", @@ -18,16 +24,11 @@ "sun", "system_health", "tag", + "timer", "updater", + "webhook", "zeroconf", - "zone", - "input_boolean", - "input_datetime", - "input_text", - "input_number", - "input_select", - "counter", - "timer" + "zone" ], "codeowners": [] } From 66efe92b3f76854f4886f24369342fc0ca62b557 Mon Sep 17 00:00:00 2001 From: Denix Date: Mon, 23 Nov 2020 20:56:50 +0100 Subject: [PATCH 209/430] Cannot use vcgencmd in HassOS (#42710) --- homeassistant/components/sensehat/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 3966e52f1a8..67beb021d89 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -1,7 +1,7 @@ """Support for Sense HAT sensors.""" from datetime import timedelta import logging -import os +from pathlib import Path from sense_hat import SenseHat import voluptuous as vol @@ -43,9 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_cpu_temp(): """Get CPU temperature.""" - res = os.popen("vcgencmd measure_temp").readline() - t_cpu = float(res.replace("temp=", "").replace("'C\n", "")) - return t_cpu + t_cpu = Path("/sys/class/thermal/thermal_zone0/temp").read_text().strip() + return float(t_cpu) * 0.001 def get_average(temp_base): From 0c30abda61df092b6114d5a7e4975a7ee1167a62 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 23 Nov 2020 21:33:14 +0100 Subject: [PATCH 210/430] Add Motion Blinds integration (#42989) Co-authored-by: J. Nick Koston --- .coveragerc | 4 + CODEOWNERS | 1 + .../components/motion_blinds/__init__.py | 101 +++++++ .../components/motion_blinds/config_flow.py | 64 +++++ .../components/motion_blinds/const.py | 6 + .../components/motion_blinds/cover.py | 256 ++++++++++++++++++ .../components/motion_blinds/gateway.py | 45 +++ .../components/motion_blinds/manifest.json | 8 + .../components/motion_blinds/sensor.py | 181 +++++++++++++ .../components/motion_blinds/strings.json | 20 ++ .../motion_blinds/translations/en.json | 20 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/motion_blinds/__init__.py | 1 + .../motion_blinds/test_config_flow.py | 75 +++++ 16 files changed, 789 insertions(+) create mode 100644 homeassistant/components/motion_blinds/__init__.py create mode 100644 homeassistant/components/motion_blinds/config_flow.py create mode 100644 homeassistant/components/motion_blinds/const.py create mode 100644 homeassistant/components/motion_blinds/cover.py create mode 100644 homeassistant/components/motion_blinds/gateway.py create mode 100644 homeassistant/components/motion_blinds/manifest.json create mode 100644 homeassistant/components/motion_blinds/sensor.py create mode 100644 homeassistant/components/motion_blinds/strings.json create mode 100644 homeassistant/components/motion_blinds/translations/en.json create mode 100644 tests/components/motion_blinds/__init__.py create mode 100644 tests/components/motion_blinds/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7186718fecd..e256be60466 100644 --- a/.coveragerc +++ b/.coveragerc @@ -542,6 +542,10 @@ omit = homeassistant/components/modbus/cover.py homeassistant/components/modbus/switch.py homeassistant/components/modem_callerid/sensor.py + homeassistant/components/motion_blinds/__init__.py + homeassistant/components/motion_blinds/const.py + homeassistant/components/motion_blinds/cover.py + homeassistant/components/motion_blinds/sensor.py homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a1de0a34518..1c63bd37c45 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,6 +277,7 @@ homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff +homeassistant/components/motion_blinds/* @starkillerOG homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core @emontnemery homeassistant/components/msteams/* @peroyvind diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py new file mode 100644 index 00000000000..72929e1ecb7 --- /dev/null +++ b/homeassistant/components/motion_blinds/__init__.py @@ -0,0 +1,101 @@ +"""The motion_blinds component.""" +from asyncio import TimeoutError as AsyncioTimeoutError +from datetime import timedelta +import logging +from socket import timeout + +from homeassistant import config_entries, core +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER +from .gateway import ConnectMotionGateway + +_LOGGER = logging.getLogger(__name__) + +MOTION_PLATFORMS = ["cover", "sensor"] + + +async def async_setup(hass: core.HomeAssistant, config: dict): + """Set up the Motion Blinds component.""" + return True + + +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up the motion_blinds components from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + host = entry.data[CONF_HOST] + key = entry.data[CONF_API_KEY] + + # Connect to motion gateway + connect_gateway_class = ConnectMotionGateway(hass) + if not await connect_gateway_class.async_connect_gateway(host, key): + raise ConfigEntryNotReady + motion_gateway = connect_gateway_class.gateway_device + + def update_gateway(): + """Call all updates using one async_add_executor_job.""" + motion_gateway.Update() + for blind in motion_gateway.device_list.values(): + blind.Update() + + async def async_update_data(): + """Fetch data from the gateway and blinds.""" + try: + await hass.async_add_executor_job(update_gateway) + except timeout as socket_timeout: + raise AsyncioTimeoutError from socket_timeout + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name=entry.title, + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=10), + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + KEY_GATEWAY: motion_gateway, + KEY_COORDINATOR: coordinator, + } + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)}, + identifiers={(DOMAIN, entry.unique_id)}, + manufacturer=MANUFACTURER, + name=entry.title, + model="Wi-Fi bridge", + sw_version=motion_gateway.protocol, + ) + + for component in MOTION_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_forward_entry_unload( + config_entry, "cover" + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py new file mode 100644 index 00000000000..fbee7d1b439 --- /dev/null +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure Motion Blinds using their WLAN API.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_HOST + +# pylint: disable=unused-import +from .const import DOMAIN +from .gateway import ConnectMotionGateway + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_GATEWAY_NAME = "Motion Gateway" + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)), + } +) + + +class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Motion Blinds config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the Motion Blinds flow.""" + self.host = None + self.key = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + self.host = user_input[CONF_HOST] + self.key = user_input[CONF_API_KEY] + return await self.async_step_connect() + + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def async_step_connect(self, user_input=None): + """Connect to the Motion Gateway.""" + + connect_gateway_class = ConnectMotionGateway(self.hass) + if not await connect_gateway_class.async_connect_gateway(self.host, self.key): + return self.async_abort(reason="connection_error") + motion_gateway = connect_gateway_class.gateway_device + + mac_address = motion_gateway.mac + + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=DEFAULT_GATEWAY_NAME, + data={CONF_HOST: self.host, CONF_API_KEY: self.key}, + ) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py new file mode 100644 index 00000000000..c80c8f881cd --- /dev/null +++ b/homeassistant/components/motion_blinds/const.py @@ -0,0 +1,6 @@ +"""Constants for the Motion Blinds component.""" +DOMAIN = "motion_blinds" +MANUFACTURER = "Motion, Coulisse B.V." + +KEY_GATEWAY = "gateway" +KEY_COORDINATOR = "coordinator" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py new file mode 100644 index 00000000000..4273be3f435 --- /dev/null +++ b/homeassistant/components/motion_blinds/cover.py @@ -0,0 +1,256 @@ +"""Support for Motion Blinds using their WLAN API.""" + +import logging + +from motionblinds import BlindType + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + CoverEntity, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +POSITION_DEVICE_MAP = { + BlindType.RollerBlind: DEVICE_CLASS_SHADE, + BlindType.RomanBlind: DEVICE_CLASS_SHADE, + BlindType.HoneycombBlind: DEVICE_CLASS_SHADE, + BlindType.DimmingBlind: DEVICE_CLASS_SHADE, + BlindType.DayNightBlind: DEVICE_CLASS_SHADE, + BlindType.RollerShutter: DEVICE_CLASS_SHUTTER, + BlindType.Switch: DEVICE_CLASS_SHUTTER, + BlindType.RollerGate: DEVICE_CLASS_GATE, + BlindType.Awning: DEVICE_CLASS_AWNING, + BlindType.Curtain: DEVICE_CLASS_CURTAIN, + BlindType.CurtainLeft: DEVICE_CLASS_CURTAIN, + BlindType.CurtainRight: DEVICE_CLASS_CURTAIN, +} + +TILT_DEVICE_MAP = { + BlindType.VenetianBlind: DEVICE_CLASS_BLIND, + BlindType.ShangriLaBlind: DEVICE_CLASS_BLIND, + BlindType.DoubleRoller: DEVICE_CLASS_SHADE, +} + +TDBU_DEVICE_MAP = { + BlindType.TopDownBottomUp: DEVICE_CLASS_SHADE, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Motion Blind from a config entry.""" + entities = [] + motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + + for blind in motion_gateway.device_list.values(): + if blind.type in POSITION_DEVICE_MAP: + entities.append( + MotionPositionDevice( + coordinator, blind, POSITION_DEVICE_MAP[blind.type], config_entry + ) + ) + + elif blind.type in TILT_DEVICE_MAP: + entities.append( + MotionTiltDevice( + coordinator, blind, TILT_DEVICE_MAP[blind.type], config_entry + ) + ) + + elif blind.type in TDBU_DEVICE_MAP: + entities.append( + MotionTDBUDevice( + coordinator, blind, TDBU_DEVICE_MAP[blind.type], config_entry, "Top" + ) + ) + entities.append( + MotionTDBUDevice( + coordinator, + blind, + TDBU_DEVICE_MAP[blind.type], + config_entry, + "Bottom", + ) + ) + + else: + _LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type) + + async_add_entities(entities) + + +class MotionPositionDevice(CoordinatorEntity, CoverEntity): + """Representation of a Motion Blind Device.""" + + def __init__(self, coordinator, blind, device_class, config_entry): + """Initialize the blind.""" + super().__init__(coordinator) + + self._blind = blind + self._device_class = device_class + self._config_entry = config_entry + + @property + def unique_id(self): + """Return the unique id of the blind.""" + return self._blind.mac + + @property + def device_info(self): + """Return the device info of the blind.""" + device_info = { + "identifiers": {(DOMAIN, self._blind.mac)}, + "manufacturer": MANUFACTURER, + "name": f"{self._blind.blind_type}-{self._blind.mac[12:]}", + "model": self._blind.blind_type, + "via_device": (DOMAIN, self._config_entry.unique_id), + } + + return device_info + + @property + def name(self): + """Return the name of the blind.""" + return f"{self._blind.blind_type}-{self._blind.mac[12:]}" + + @property + def current_cover_position(self): + """ + Return current position of cover. + + None is unknown, 0 is open, 100 is closed. + """ + if self._blind.position is None: + return None + return 100 - self._blind.position + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self._blind.position == 100 + + def open_cover(self, **kwargs): + """Open the cover.""" + self._blind.Open() + + def close_cover(self, **kwargs): + """Close cover.""" + self._blind.Close() + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + self._blind.Set_position(100 - position) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._blind.Stop() + + +class MotionTiltDevice(MotionPositionDevice): + """Representation of a Motion Blind Device.""" + + @property + def current_cover_tilt_position(self): + """ + Return current angle of cover. + + None is unknown, 0 is closed/minimum tilt, 100 is fully open/maximum tilt. + """ + if self._blind.angle is None: + return None + return self._blind.angle * 100 / 180 + + def open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + self._blind.Set_angle(180) + + def close_cover_tilt(self, **kwargs): + """Close the cover tilt.""" + self._blind.Set_angle(0) + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 + self._blind.Set_angle(angle) + + def stop_cover_tilt(self, **kwargs): + """Stop the cover.""" + self._blind.Stop() + + +class MotionTDBUDevice(MotionPositionDevice): + """Representation of a Motion Top Down Bottom Up blind Device.""" + + def __init__(self, coordinator, blind, device_class, config_entry, motor): + """Initialize the blind.""" + super().__init__(coordinator, blind, device_class, config_entry) + self._motor = motor + self._motor_key = motor[0] + + if self._motor not in ["Bottom", "Top"]: + _LOGGER.error("Unknown motor '%s'", self._motor) + + @property + def unique_id(self): + """Return the unique id of the blind.""" + return f"{self._blind.mac}-{self._motor}" + + @property + def name(self): + """Return the name of the blind.""" + return f"{self._blind.blind_type}-{self._motor}-{self._blind.mac[12:]}" + + @property + def current_cover_position(self): + """ + Return current position of cover. + + None is unknown, 0 is open, 100 is closed. + """ + if self._blind.position is None: + return None + + return 100 - self._blind.position[self._motor_key] + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._blind.position is None: + return None + + return self._blind.position[self._motor_key] == 100 + + def open_cover(self, **kwargs): + """Open the cover.""" + self._blind.Open(motor=self._motor_key) + + def close_cover(self, **kwargs): + """Close cover.""" + self._blind.Close(motor=self._motor_key) + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + self._blind.Set_position(100 - position, motor=self._motor_key) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._blind.Stop(motor=self._motor_key) diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py new file mode 100644 index 00000000000..e7e665d65f9 --- /dev/null +++ b/homeassistant/components/motion_blinds/gateway.py @@ -0,0 +1,45 @@ +"""Code to handle a Motion Gateway.""" +import logging +import socket + +from motionblinds import MotionGateway + +_LOGGER = logging.getLogger(__name__) + + +class ConnectMotionGateway: + """Class to async connect to a Motion Gateway.""" + + def __init__(self, hass): + """Initialize the entity.""" + self._hass = hass + self._gateway_device = None + + @property + def gateway_device(self): + """Return the class containing all connections to the gateway.""" + return self._gateway_device + + def update_gateway(self): + """Update all information of the gateway.""" + self.gateway_device.GetDeviceList() + self.gateway_device.Update() + + async def async_connect_gateway(self, host, key): + """Connect to the Motion Gateway.""" + _LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3]) + self._gateway_device = MotionGateway(ip=host, key=key) + try: + # update device info and get the connected sub devices + await self._hass.async_add_executor_job(self.update_gateway) + except socket.timeout: + _LOGGER.error( + "Timeout trying to connect to Motion Gateway with host %s", host + ) + return False + _LOGGER.debug( + "Motion gateway mac: %s, protocol: %s detected", + self.gateway_device.mac, + self.gateway_device.protocol, + ) + return True diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json new file mode 100644 index 00000000000..84cf711ac97 --- /dev/null +++ b/homeassistant/components/motion_blinds/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "motion_blinds", + "name": "Motion Blinds", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/motion_blinds", + "requirements": ["motionblinds==0.1.6"], + "codeowners": ["@starkillerOG"] +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py new file mode 100644 index 00000000000..81d555806ed --- /dev/null +++ b/homeassistant/components/motion_blinds/sensor.py @@ -0,0 +1,181 @@ +"""Support for Motion Blinds sensors.""" +import logging + +from motionblinds import BlindType + +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY + +_LOGGER = logging.getLogger(__name__) + +ATTR_BATTERY_VOLTAGE = "battery_voltage" +TYPE_BLIND = "blind" +TYPE_GATEWAY = "gateway" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Perform the setup for Motion Blinds.""" + entities = [] + motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + + for blind in motion_gateway.device_list.values(): + entities.append(MotionSignalStrengthSensor(coordinator, blind, TYPE_BLIND)) + if blind.type == BlindType.TopDownBottomUp: + entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom")) + entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top")) + elif blind.battery_voltage > 0: + # Only add battery powered blinds + entities.append(MotionBatterySensor(coordinator, blind)) + + entities.append( + MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY) + ) + + async_add_entities(entities) + + +class MotionBatterySensor(CoordinatorEntity, Entity): + """ + Representation of a Motion Battery Sensor. + + Updates are done by the cover platform. + """ + + def __init__(self, coordinator, blind): + """Initialize the Motion Battery Sensor.""" + super().__init__(coordinator) + + self._blind = blind + + @property + def unique_id(self): + """Return the unique id of the blind.""" + return f"{self._blind.mac}-battery" + + @property + def device_info(self): + """Return the device info of the blind.""" + return {"identifiers": {(DOMAIN, self._blind.mac)}} + + @property + def name(self): + """Return the name of the blind battery sensor.""" + return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return PERCENTAGE + + @property + def device_class(self): + """Return the device class of this entity.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self): + """Return the state of the sensor.""" + return self._blind.battery_level + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} + + +class MotionTDBUBatterySensor(MotionBatterySensor): + """ + Representation of a Motion Battery Sensor for a Top Down Bottom Up blind. + + Updates are done by the cover platform. + """ + + def __init__(self, coordinator, blind, motor): + """Initialize the Motion Battery Sensor.""" + super().__init__(coordinator, blind) + + self._motor = motor + + @property + def unique_id(self): + """Return the unique id of the blind.""" + return f"{self._blind.mac}-{self._motor}-battery" + + @property + def name(self): + """Return the name of the blind battery sensor.""" + return f"{self._blind.blind_type}-{self._motor}-battery-{self._blind.mac[12:]}" + + @property + def state(self): + """Return the state of the sensor.""" + if self._blind.battery_level is None: + return None + return self._blind.battery_level[self._motor[0]] + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attributes = {} + if self._blind.battery_voltage is not None: + attributes[ATTR_BATTERY_VOLTAGE] = self._blind.battery_voltage[ + self._motor[0] + ] + return attributes + + +class MotionSignalStrengthSensor(CoordinatorEntity, Entity): + """Representation of a Motion Signal Strength Sensor.""" + + def __init__(self, coordinator, device, device_type): + """Initialize the Motion Signal Strength Sensor.""" + super().__init__(coordinator) + + self._device = device + self._device_type = device_type + + @property + def unique_id(self): + """Return the unique id of the blind.""" + return f"{self._device.mac}-RSSI" + + @property + def device_info(self): + """Return the device info of the blind.""" + return {"identifiers": {(DOMAIN, self._device.mac)}} + + @property + def name(self): + """Return the name of the blind signal strength sensor.""" + if self._device_type == TYPE_GATEWAY: + return "Motion gateway signal strength" + return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SIGNAL_STRENGTH_DECIBELS_MILLIWATT + + @property + def device_class(self): + """Return the device class of this entity.""" + return DEVICE_CLASS_SIGNAL_STRENGTH + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.RSSI diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json new file mode 100644 index 00000000000..d9c8a4099ac --- /dev/null +++ b/homeassistant/components/motion_blinds/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "Motion Blinds", + "step": { + "user": { + "title": "Motion Blinds", + "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json new file mode 100644 index 00000000000..2224ea38601 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for this Motion gateway is already in progress", + "connection_error": "Failed to connect, please try again" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "host": "IP address", + "api_key": "API key" + }, + "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 531e575dc0a..02fdac9ed3e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,6 +121,7 @@ FLOWS = [ "minecraft_server", "mobile_app", "monoprice", + "motion_blinds", "mqtt", "myq", "neato", diff --git a/requirements_all.txt b/requirements_all.txt index 2990802b2f0..8fd652e7628 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -951,6 +951,9 @@ minio==4.0.9 # homeassistant.components.mitemp_bt mitemp_bt==0.0.3 +# homeassistant.components.motion_blinds +motionblinds==0.1.6 + # homeassistant.components.tts mutagen==1.45.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 006c2c78de6..bdc982b1b23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -473,6 +473,9 @@ millheater==0.4.0 # homeassistant.components.minio minio==4.0.9 +# homeassistant.components.motion_blinds +motionblinds==0.1.6 + # homeassistant.components.tts mutagen==1.45.1 diff --git a/tests/components/motion_blinds/__init__.py b/tests/components/motion_blinds/__init__.py new file mode 100644 index 00000000000..1c77ce16922 --- /dev/null +++ b/tests/components/motion_blinds/__init__.py @@ -0,0 +1 @@ +"""Tests for the Motion Blinds integration.""" diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py new file mode 100644 index 00000000000..faa3e7115b8 --- /dev/null +++ b/tests/components/motion_blinds/test_config_flow.py @@ -0,0 +1,75 @@ +"""Test the Motion Blinds config flow.""" +import socket + +import pytest + +from homeassistant import config_entries +from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME +from homeassistant.components.motion_blinds.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST + +from tests.async_mock import patch + +TEST_HOST = "1.2.3.4" +TEST_API_KEY = "12ab345c-d67e-8f" + + +@pytest.fixture(name="motion_blinds_connect", autouse=True) +def motion_blinds_connect_fixture(): + """Mock motion blinds connection and entry setup.""" + with patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", + return_value=True, + ), patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.Update", + return_value=True, + ), patch( + "homeassistant.components.motion_blinds.async_setup_entry", return_value=True + ): + yield + + +async def test_config_flow_manual_host_success(hass): + """Successful flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + } + + +async def test_config_flow_connection_error(hass): + """Failed flow manually initialized by the user with connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", + side_effect=socket.timeout, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "connection_error" From 059f1a35f2c39ff2d384559f5ede090946e96e7a Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 23 Nov 2020 21:34:46 +0100 Subject: [PATCH 211/430] Use references in config flow for solaredge (#43511) --- homeassistant/components/solaredge/config_flow.py | 13 ++++++------- homeassistant/components/solaredge/strings.json | 9 ++++++--- tests/components/solaredge/test_config_flow.py | 6 +++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 8a7f2af3a99..49c265b4221 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -41,15 +41,14 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 (ConnectTimeout, HTTPError): + self._errors[CONF_SITE_ID] = "could_not_connect" + return False except KeyError: - self._errors[CONF_SITE_ID] = "api_failure" + self._errors[CONF_SITE_ID] = "invalid_api_key" return False return True @@ -59,7 +58,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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" + self._errors[CONF_SITE_ID] = "already_configured" else: site = user_input[CONF_SITE_ID] api = user_input[CONF_API_KEY] @@ -94,5 +93,5 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 self.async_abort(reason="already_configured") return await self.async_step_user(user_input) diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json index eb4c5cda1fd..b6f258b0dc8 100644 --- a/homeassistant/components/solaredge/strings.json +++ b/homeassistant/components/solaredge/strings.json @@ -11,10 +11,13 @@ } }, "error": { - "site_exists": "This site_id is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "site_not_active": "The site is not active", + "could_not_connect": "Could not connect to the solaredge API" }, "abort": { - "site_exists": "This site_id is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 61bc5f9ac6c..835fc300982 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -85,14 +85,14 @@ async def test_abort_if_already_setup(hass, test_api): {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" + assert result["reason"] == "already_configured" # 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"} + assert result["errors"] == {CONF_SITE_ID: "already_configured"} async def test_asserts(hass, test_api): @@ -113,7 +113,7 @@ async def test_asserts(hass, test_api): {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"} + assert result["errors"] == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout test_api.get_details.side_effect = ConnectTimeout() From c157fbef1e88b3f34fe230fb153f362b7d187215 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Nov 2020 22:06:59 +0100 Subject: [PATCH 212/430] Bump version to 0.119.0dev0 (#43485) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d60e443818e..8917aa003a4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 118 +MINOR_VERSION = 119 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From fcd8684d40db9677aa0cc43bc332746a02f4e5e3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 24 Nov 2020 00:04:19 +0000 Subject: [PATCH 213/430] [ci skip] Translation update --- homeassistant/components/adguard/translations/pl.json | 2 +- homeassistant/components/arcam_fmj/translations/pl.json | 2 +- homeassistant/components/blebox/translations/pl.json | 2 +- homeassistant/components/braviatv/translations/pl.json | 2 +- homeassistant/components/brother/translations/pl.json | 2 +- homeassistant/components/bsblan/translations/pl.json | 2 +- homeassistant/components/deconz/translations/pl.json | 4 ++-- homeassistant/components/dialogflow/translations/pl.json | 2 +- homeassistant/components/elgato/translations/pl.json | 4 ++-- homeassistant/components/esphome/translations/pl.json | 2 +- homeassistant/components/geofency/translations/pl.json | 2 +- homeassistant/components/gpslogger/translations/pl.json | 2 +- homeassistant/components/huawei_lte/translations/pl.json | 2 +- homeassistant/components/ifttt/translations/pl.json | 2 +- homeassistant/components/insteon/translations/pl.json | 2 +- homeassistant/components/ipp/translations/pl.json | 4 ++-- homeassistant/components/kodi/translations/no.json | 1 + homeassistant/components/kodi/translations/pl.json | 2 +- homeassistant/components/konnected/translations/pl.json | 2 +- homeassistant/components/locative/translations/pl.json | 2 +- homeassistant/components/mailgun/translations/pl.json | 2 +- homeassistant/components/mobile_app/translations/pl.json | 2 +- .../components/motion_blinds/translations/en.json | 8 ++++---- homeassistant/components/mqtt/translations/pl.json | 2 +- homeassistant/components/ovo_energy/translations/no.json | 5 ++++- homeassistant/components/plaato/translations/pl.json | 2 +- homeassistant/components/risco/translations/pl.json | 6 +++--- homeassistant/components/samsungtv/translations/pl.json | 4 ++-- homeassistant/components/smappee/translations/pl.json | 4 ++-- homeassistant/components/smarthab/translations/pl.json | 2 +- homeassistant/components/smartthings/translations/pl.json | 8 ++++---- homeassistant/components/solaredge/translations/en.json | 7 ++++++- homeassistant/components/srp_energy/translations/no.json | 1 + homeassistant/components/traccar/translations/pl.json | 2 +- homeassistant/components/twilio/translations/pl.json | 2 +- homeassistant/components/vacuum/translations/no.json | 2 +- homeassistant/components/vera/translations/pl.json | 4 ++-- homeassistant/components/vizio/translations/pl.json | 2 +- homeassistant/components/volumio/translations/pl.json | 2 +- homeassistant/components/wled/translations/pl.json | 4 ++-- 40 files changed, 63 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index 2f8a134c04d..41cb2c019dd 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?", "title": "AdGuard Home przez dodatek Hass.io" }, "user": { diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json index 8cf2bb5b4eb..af28552d892 100644 --- a/homeassistant/components/arcam_fmj/translations/pl.json +++ b/homeassistant/components/arcam_fmj/translations/pl.json @@ -14,7 +14,7 @@ "flow_title": "Arcam FMJ na {host}", "step": { "confirm": { - "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistant?" + "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistanta?" }, "user": { "data": { diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json index 4cdd35f4ac3..9f61e308f64 100644 --- a/homeassistant/components/blebox/translations/pl.json +++ b/homeassistant/components/blebox/translations/pl.json @@ -16,7 +16,7 @@ "host": "Adres IP", "port": "Port" }, - "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistant.", + "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistantem.", "title": "Konfiguracja urz\u0105dzenia BleBox" } } diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 0088cf604ad..1aa5d7cb58a 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -14,7 +14,7 @@ "data": { "pin": "Kod PIN" }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistant na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", + "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", "title": "Autoryzacja Sony Bravia TV" }, "user": { diff --git a/homeassistant/components/brother/translations/pl.json b/homeassistant/components/brother/translations/pl.json index 9b62e9afed4..c4c1c3d7d7a 100644 --- a/homeassistant/components/brother/translations/pl.json +++ b/homeassistant/components/brother/translations/pl.json @@ -22,7 +22,7 @@ "data": { "type": "Typ drukarki" }, - "description": "Czy chcesz doda\u0107 drukark\u0119 Brother {model} o numerze seryjnym `{serial_number}` do Home Assistant?", + "description": "Czy chcesz doda\u0107 drukark\u0119 Brother {model} o numerze seryjnym `{serial_number}` do Home Assistanta?", "title": "Wykryto drukark\u0119 Brother" } } diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json index f95f2695883..5ab1eabbe26 100644 --- a/homeassistant/components/bsblan/translations/pl.json +++ b/homeassistant/components/bsblan/translations/pl.json @@ -14,7 +14,7 @@ "passkey": "Ci\u0105g klucza dost\u0119pu", "port": "Port" }, - "description": "Konfiguracja urz\u0105dzenia BSB-LAN w celu integracji z Home Assistant.", + "description": "Konfiguracja urz\u0105dzenia BSB-LAN w celu integracji z Home Assistantem.", "title": "Po\u0142\u0105czenie z urz\u0105dzeniem BSB-Lan" } } diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index e6c89d53b4a..24a3ba61706 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -14,11 +14,11 @@ "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "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}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, 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" }, "link": { - "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\"", + "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistantem. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", "title": "Po\u0142\u0105czenie z deCONZ" }, "manual_input": { diff --git a/homeassistant/components/dialogflow/translations/pl.json b/homeassistant/components/dialogflow/translations/pl.json index 031c69c6eca..c90ed20af74 100644 --- a/homeassistant/components/dialogflow/translations/pl.json +++ b/homeassistant/components/dialogflow/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, 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/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json index 8d36d06f501..37a6b94a5b1 100644 --- a/homeassistant/components/elgato/translations/pl.json +++ b/homeassistant/components/elgato/translations/pl.json @@ -14,10 +14,10 @@ "host": "Nazwa hosta lub adres IP", "port": "Port" }, - "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistant." + "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistantem." }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistant?", + "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistanta?", "title": "Wykryto urz\u0105dzenie Elgato Key Light" } } diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index 1866b372eb4..34b8d9bd0e1 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -18,7 +18,7 @@ "description": "Wprowad\u017a has\u0142o ustawione w konfiguracji dla {name}." }, "discovery_confirm": { - "description": "Czy chcesz doda\u0107 w\u0119ze\u0142 ESPHome `{name}` do Home Assistant?", + "description": "Czy chcesz doda\u0107 w\u0119ze\u0142 ESPHome `{name}` do Home Assistanta?", "title": "Znaleziono w\u0119ze\u0142 ESPHome" }, "user": { diff --git a/homeassistant/components/geofency/translations/pl.json b/homeassistant/components/geofency/translations/pl.json index b43dbabb806..c504e31051a 100644 --- a/homeassistant/components/geofency/translations/pl.json +++ b/homeassistant/components/geofency/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, musisz skonfigurowa\u0107 webhook w Geofency. \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 Assistanta, musisz skonfigurowa\u0107 webhook w Geofency. \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/gpslogger/translations/pl.json b/homeassistant/components/gpslogger/translations/pl.json index a7f40dbeb9f..f868f770c5d 100644 --- a/homeassistant/components/gpslogger/translations/pl.json +++ b/homeassistant/components/gpslogger/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant, musisz skonfigurowa\u0107 webhook w aplikacji GPSLogger. \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 lokalizacje do Home Assistanta, musisz skonfigurowa\u0107 webhook w aplikacji GPSLogger. \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/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 0cc6269c05d..0720182b697 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -23,7 +23,7 @@ "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 zewn\u0105trz Home Assistant, gdy integracja jest aktywna.", + "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 Assistanta, gdy integracja jest aktywna.", "title": "Konfiguracja Huawei LTE" } } diff --git a/homeassistant/components/ifttt/translations/pl.json b/homeassistant/components/ifttt/translations/pl.json index 7c0dfbfab66..d8d7bff1585 100644 --- a/homeassistant/components/ifttt/translations/pl.json +++ b/homeassistant/components/ifttt/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, musisz u\u017cy\u0107 akcji \"Make a web request\" z [apletu IFTTT Webhook]({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 Assistanta, musisz u\u017cy\u0107 akcji \"Make a web request\" z [apletu IFTTT Webhook]({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/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index 28c4414b9da..c4a58e0e09a 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -76,7 +76,7 @@ "port": "Port", "username": "Nazwa u\u017cytkownika" }, - "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany musisz ponownie uruchomi\u0107 Home Assistant. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub.", + "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany musisz ponownie uruchomi\u0107 Home Assistanta. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub.", "title": "Insteon" }, "init": { diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index fa8e9a3ca0b..8a058c9b9d6 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -23,11 +23,11 @@ "ssl": "Certyfikat SSL", "verify_ssl": "Weryfikacja certyfikatu SSL" }, - "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol) w celu integracji z Home Assistant.", + "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol) w celu integracji z Home Assistantem.", "title": "Po\u0142\u0105cz swoj\u0105 drukark\u0119" }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistant?", + "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistanta?", "title": "Wykryto drukark\u0119" } } diff --git a/homeassistant/components/kodi/translations/no.json b/homeassistant/components/kodi/translations/no.json index 594daf99e66..b7815c3aa3a 100644 --- a/homeassistant/components/kodi/translations/no.json +++ b/homeassistant/components/kodi/translations/no.json @@ -4,6 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", + "no_uuid": "Kodi-forekomsten har ikke en unik id. Dette skyldes mest sannsynlig en gammel Kodi-versjon (17.x eller under). Du kan konfigurere integreringen manuelt eller oppgradere til en nyere Kodi-versjon.", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index a1f0a3a6eed..2194c480c03 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -22,7 +22,7 @@ "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o Kodi. Mo\u017cna je znale\u017a\u0107 w System/Ustawienia/Sie\u0107/Us\u0142ugi." }, "discovery_confirm": { - "description": "Czy chcesz doda\u0107 Kodi (\"{name}\") do Home Assistant?", + "description": "Czy chcesz doda\u0107 Kodi (\"{name}\") do Home Assistanta?", "title": "Wykryte urz\u0105dzenia Kodi" }, "user": { diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index a6a2a715237..ee6c10cbdd8 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -90,7 +90,7 @@ "api_host": "Zast\u0119powanie adresu URL hosta API (opcjonalnie)", "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu", "discovery": "Odpowiadaj na \u017c\u0105dania wykrywania w Twojej sieci", - "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistant" + "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistanta" }, "description": "Wybierz po\u017c\u0105dane zachowanie dla swojego panelu", "title": "R\u00f3\u017cne opcje" diff --git a/homeassistant/components/locative/translations/pl.json b/homeassistant/components/locative/translations/pl.json index 018bca1f99b..f91afa32f74 100644 --- a/homeassistant/components/locative/translations/pl.json +++ b/homeassistant/components/locative/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant, musisz skonfigurowa\u0107 webhook w aplikacji Locative. \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 lokalizacje do Home Assistanta, musisz skonfigurowa\u0107 webhook w aplikacji Locative. \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/mailgun/translations/pl.json b/homeassistant/components/mailgun/translations/pl.json index fac09505ffa..931cd7a8167 100644 --- a/homeassistant/components/mailgun/translations/pl.json +++ b/homeassistant/components/mailgun/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_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 Assistanta, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_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/mobile_app/translations/pl.json b/homeassistant/components/mobile_app/translations/pl.json index 15574f6d757..c62d2c81076 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 Assistantem. Zapoznaj si\u0119 z [dokumentacj\u0105]({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji." }, "step": { "confirm": { diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 2224ea38601..b7830a255fc 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Device is already configured", - "already_in_progress": "Config flow for this Motion gateway is already in progress", - "connection_error": "Failed to connect, please try again" + "already_in_progress": "Configuration flow is already in progress", + "connection_error": "Failed to connect" }, "flow_title": "Motion Blinds", "step": { "user": { "data": { - "host": "IP address", - "api_key": "API key" + "api_key": "API Key", + "host": "IP Address" }, "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "title": "Motion Blinds" diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 2c11afd52e9..ce41d059b24 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -21,7 +21,7 @@ "data": { "discovery": "W\u0142\u0105cz wykrywanie" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", "title": "Po\u015brednik MQTT za po\u015brednictwem dodatku Hass.io" } } diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 99cc17cc348..5e0537b6633 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -5,11 +5,14 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { "data": { "password": "Passord" - } + }, + "description": "Autentisering mislyktes for OVO Energy. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", + "title": "Reautorisasjon" }, "user": { "data": { diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index 0abb0df4142..1f7c8141aa5 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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 Assistanta, 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/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index 92deb2da70a..ef7ed9f13e0 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -28,7 +28,7 @@ "armed_night": "Uzbrojony (noc)" }, "description": "Wybierz stan, w kt\u00f3rym chcesz ustawi\u0107 alarm Risco podczas uzbrajania alarmu w Home Assistant", - "title": "Mapuj stany Home Assistant do stan\u00f3w alarmu Risco" + "title": "Mapuj stany Home Assistanta do stan\u00f3w alarmu Risco" }, "init": { "data": { @@ -47,8 +47,8 @@ "arm": "Uzbrojony (pod nieobecno\u015b\u0107)", "partial_arm": "Uzbrojony (obecny)" }, - "description": "Wybierz stan, kt\u00f3ry b\u0119dzie raportowa\u0142 alarm Home Assistant, dla ka\u017cdego stanu zg\u0142oszonego przez Risco", - "title": "Mapuj stany Risco do stan\u00f3w Home Assistant" + "description": "Wybierz stan, kt\u00f3ry b\u0119dzie raportowa\u0142 alarm Home Assistanta, dla ka\u017cdego stanu zg\u0142oszonego przez Risco", + "title": "Mapuj stany Risco do stan\u00f3w Home Assistanta" } } } diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 0257827abed..07751797f85 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -10,7 +10,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", + "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", "title": "Samsung TV" }, "user": { @@ -18,7 +18,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Wprowad\u017a informacje o telewizorze Samsung. Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant." + "description": "Wprowad\u017a informacje o telewizorze Samsung. Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie." } } } diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json index 56f20551195..ac3393e51e1 100644 --- a/homeassistant/components/smappee/translations/pl.json +++ b/homeassistant/components/smappee/translations/pl.json @@ -15,7 +15,7 @@ "data": { "environment": "\u015arodowisko" }, - "description": "Skonfiguruj Smappee, aby zintegrowa\u0107 go z Home Assistant." + "description": "Skonfiguruj Smappee, aby zintegrowa\u0107 go z Home Assistantem." }, "local": { "data": { @@ -27,7 +27,7 @@ "title": "Wybierz metod\u0119 uwierzytelniania" }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 do Home Assistant urz\u0105dzenie Smappee o numerze seryjnym `{serialnumber}`?", + "description": "Czy chcesz doda\u0107 do Home Assistanta urz\u0105dzenie Smappee o numerze seryjnym `{serialnumber}`?", "title": "Wykryto urz\u0105dzenie Smappee" } } diff --git a/homeassistant/components/smarthab/translations/pl.json b/homeassistant/components/smarthab/translations/pl.json index 7b2d65b85ab..14ca88f1c00 100644 --- a/homeassistant/components/smarthab/translations/pl.json +++ b/homeassistant/components/smarthab/translations/pl.json @@ -11,7 +11,7 @@ "email": "Adres e-mail", "password": "Has\u0142o" }, - "description": "Ze wzgl\u0119d\u00f3w technicznych, nale\u017cy u\u017cy\u0107 dodatkowego konta, specjalnie na u\u017cytek dla Home Assistant. Mo\u017cesz je utworzy\u0107 z poziomu aplikacji SmartHab.", + "description": "Ze wzgl\u0119d\u00f3w technicznych, nale\u017cy u\u017cy\u0107 dodatkowego konta, specjalnie na u\u017cytek dla Home Assistanta. Mo\u017cesz je utworzy\u0107 z poziomu aplikacji SmartHab.", "title": "Konfiguracja SmartHab" } } diff --git a/homeassistant/components/smartthings/translations/pl.json b/homeassistant/components/smartthings/translations/pl.json index 6696235ab8f..74201e76fbd 100644 --- a/homeassistant/components/smartthings/translations/pl.json +++ b/homeassistant/components/smartthings/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant nie jest poprawnie skonfigurowany do otrzymywania danych od SmartThings. Adres URL webhook jest nieprawid\u0142owy: \n > {webhook_url} \n\n Zaktualizuj konfiguracj\u0119 zgodnie z [instrukcj\u0105]({component_url}), uruchom ponownie Home Assistant i spr\u00f3buj ponownie.", + "invalid_webhook_url": "Home Assistant nie jest poprawnie skonfigurowany do otrzymywania danych od SmartThings. Adres URL webhook jest nieprawid\u0142owy: \n > {webhook_url} \n\n Zaktualizuj konfiguracj\u0119 zgodnie z [instrukcj\u0105]({component_url}), uruchom ponownie Home Assistanta i spr\u00f3buj ponownie.", "no_available_locations": "Nie ma dost\u0119pnych lokalizacji SmartThings do skonfigurowania w Home Assistant" }, "error": { @@ -13,7 +13,7 @@ }, "step": { "authorize": { - "title": "Autoryzuj Home Assistant" + "title": "Autoryzuj Home Assistanta" }, "pat": { "data": { @@ -26,11 +26,11 @@ "data": { "location_id": "Lokalizacja" }, - "description": "Wybierz lokalizacj\u0119 SmartThings, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistant. Nast\u0119pnie otwarte zostanie nowe okno i zostaniesz poproszony o zalogowanie si\u0119 i autoryzacj\u0119 instalacji integracji Home Assistant w wybranej lokalizacji.", + "description": "Wybierz lokalizacj\u0119 SmartThings, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta. Nast\u0119pnie otwarte zostanie nowe okno i zostaniesz poproszony o zalogowanie si\u0119 i autoryzacj\u0119 instalacji integracji Home Assistant w wybranej lokalizacji.", "title": "Wybierz lokalizacj\u0119" }, "user": { - "description": "SmartThings zostanie skonfigurowany, by wysy\u0142a\u0107 aktualizacje push do Home Assistant na:\n> {webhook_url}\n\nJe\u015bli adres jest nieprawid\u0142owy, popraw swoj\u0105 konfiguracj\u0119, uruchom ponownie Home Assistant i spr\u00f3buj ponownie.", + "description": "SmartThings zostanie skonfigurowany, by wysy\u0142a\u0107 aktualizacje push do Home Assistanta na:\n> {webhook_url}\n\nJe\u015bli adres jest nieprawid\u0142owy, popraw swoj\u0105 konfiguracj\u0119, uruchom ponownie Home Assistant i spr\u00f3buj ponownie.", "title": "Potwierd\u017a Callback URL" } } diff --git a/homeassistant/components/solaredge/translations/en.json b/homeassistant/components/solaredge/translations/en.json index d3d1fb4e862..a9abfd5e013 100644 --- a/homeassistant/components/solaredge/translations/en.json +++ b/homeassistant/components/solaredge/translations/en.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "site_exists": "This site_id is already configured" }, "error": { - "site_exists": "This site_id is already configured" + "already_configured": "Device is already configured", + "could_not_connect": "Could not connect to the solaredge API", + "invalid_api_key": "Invalid API key", + "site_exists": "This site_id is already configured", + "site_not_active": "The site is not active" }, "step": { "user": { diff --git a/homeassistant/components/srp_energy/translations/no.json b/homeassistant/components/srp_energy/translations/no.json index a3177325024..5505e140cd3 100644 --- a/homeassistant/components/srp_energy/translations/no.json +++ b/homeassistant/components/srp_energy/translations/no.json @@ -13,6 +13,7 @@ "user": { "data": { "id": "Konto ID", + "is_tou": "Er Time of Use Plan", "password": "Passord", "username": "Brukernavn" } diff --git a/homeassistant/components/traccar/translations/pl.json b/homeassistant/components/traccar/translations/pl.json index 7b990190d84..619f6e57192 100644 --- a/homeassistant/components/traccar/translations/pl.json +++ b/homeassistant/components/traccar/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, 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/twilio/translations/pl.json b/homeassistant/components/twilio/translations/pl.json index 667dddd747e..e6be0a02aed 100644 --- a/homeassistant/components/twilio/translations/pl.json +++ b/homeassistant/components/twilio/translations/pl.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \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 Assistanta, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \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/vacuum/translations/no.json b/homeassistant/components/vacuum/translations/no.json index f39e62058d7..3d722c0927c 100644 --- a/homeassistant/components/vacuum/translations/no.json +++ b/homeassistant/components/vacuum/translations/no.json @@ -9,7 +9,7 @@ "is_docked": "{entity_name} er dokket" }, "trigger_type": { - "cleaning": "{entity_name} startet rengj\u00f8ringen", + "cleaning": "{entity_name} startet rengj\u00f8ring", "docked": "{entity_name} dokket" } }, diff --git a/homeassistant/components/vera/translations/pl.json b/homeassistant/components/vera/translations/pl.json index e7a6696c867..5b7b6886085 100644 --- a/homeassistant/components/vera/translations/pl.json +++ b/homeassistant/components/vera/translations/pl.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistant.", + "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistanta.", "lights": "Identyfikatory prze\u0142\u0105cznik\u00f3w Vera, kt\u00f3re maj\u0105 by\u0107 traktowane jako \u015bwiat\u0142a w Home Assistant.", "vera_controller_url": "Adres URL kontrolera" }, @@ -19,7 +19,7 @@ "step": { "init": { "data": { - "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistant.", + "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistanta.", "lights": "Identyfikatory prze\u0142\u0105cznik\u00f3w Vera, kt\u00f3re maj\u0105 by\u0107 traktowane jako \u015bwiat\u0142a w Home Assistant." }, "description": "Szczeg\u00f3\u0142owe informacje na temat parametr\u00f3w opcjonalnych mo\u017cna znale\u017a\u0107 w dokumentacji Vera: https://www.home-assistant.io/integrations/vera/. Uwaga: Wszelkie zmiany tutaj b\u0119d\u0105 wymaga\u0142y ponownego uruchomienia serwera Home Assistant. Aby wyczy\u015bci\u0107 warto\u015bci, wpisz spacj\u0119.", diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 71754f6fbac..82339204a16 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -19,7 +19,7 @@ "title": "Ko\u0144czenie procesu parowania" }, "pairing_complete": { - "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistant.", + "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistantem.", "title": "Parowanie zako\u0144czone" }, "pairing_complete_import": { diff --git a/homeassistant/components/volumio/translations/pl.json b/homeassistant/components/volumio/translations/pl.json index 2a99bec962d..67d49c4b4be 100644 --- a/homeassistant/components/volumio/translations/pl.json +++ b/homeassistant/components/volumio/translations/pl.json @@ -10,7 +10,7 @@ }, "step": { "discovery_confirm": { - "description": "Czy chcesz doda\u0107 Volumio (\"{name}\") do Home Assistant?", + "description": "Czy chcesz doda\u0107 Volumio (\"{name}\") do Home Assistanta?", "title": "Wykryte Volumio" }, "user": { diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index f73e7d05651..6552b6de239 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -13,10 +13,10 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Konfiguracja WLED w celu integracji z Home Assistant." + "description": "Konfiguracja WLED w celu integracji z Home Assistantem." }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistant?", + "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistanta?", "title": "Wykryto urz\u0105dzenie WLED" } } From 8f18af9e80b57a222494c07a418faa86f42908fa Mon Sep 17 00:00:00 2001 From: JJdeVries <43748187+JJdeVries@users.noreply.github.com> Date: Tue, 24 Nov 2020 04:50:57 +0100 Subject: [PATCH 214/430] Decrease asuswrt connects per sensor (#43383) * asuswrt: Decrease number of connects per sensor * Changed implementation to DataUpdateCoordinator * review changes * update aioasuswrt dependency * Apply suggestions from code review Add explicit return None and fix type hint Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/asuswrt/manifest.json | 2 +- homeassistant/components/asuswrt/sensor.py | 281 ++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/asuswrt/test_sensor.py | 5 - 5 files changed, 133 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 26b8d49ddb1..9afb7849f8c 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -2,6 +2,6 @@ "domain": "asuswrt", "name": "ASUSWRT", "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": ["aioasuswrt==1.3.0"], + "requirements": ["aioasuswrt==1.3.1"], "codeowners": ["@kennedyshead"] } diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index f226b953c53..15ca58a525f 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,184 +1,163 @@ """Asuswrt status sensors.""" +from datetime import timedelta +import enum import logging +from typing import Any, Dict, List, Optional from aioasuswrt.asuswrt import AsusWrt from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import DATA_ASUSWRT -_LOGGER = logging.getLogger(__name__) - UPLOAD_ICON = "mdi:upload-network" DOWNLOAD_ICON = "mdi:download-network" +_LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): + +@enum.unique +class _SensorTypes(enum.Enum): + DEVICES = "devices" + UPLOAD = "upload" + DOWNLOAD = "download" + DOWNLOAD_SPEED = "download_speed" + UPLOAD_SPEED = "upload_speed" + + @property + def unit(self) -> Optional[str]: + """Return a string with the unit of the sensortype.""" + if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD): + return DATA_GIGABYTES + if self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED): + return DATA_RATE_MEGABITS_PER_SECOND + return None + + @property + def icon(self) -> Optional[str]: + """Return the expected icon for the sensortype.""" + if self in (_SensorTypes.UPLOAD, _SensorTypes.UPLOAD_SPEED): + return UPLOAD_ICON + if self in (_SensorTypes.DOWNLOAD, _SensorTypes.DOWNLOAD_SPEED): + return DOWNLOAD_ICON + return None + + @property + def sensor_name(self) -> Optional[str]: + """Return the name of the sensor.""" + if self is _SensorTypes.DEVICES: + return "Asuswrt Devices Connected" + if self is _SensorTypes.UPLOAD: + return "Asuswrt Upload" + if self is _SensorTypes.DOWNLOAD: + return "Asuswrt Download" + if self is _SensorTypes.UPLOAD_SPEED: + return "Asuswrt Upload Speed" + if self is _SensorTypes.DOWNLOAD_SPEED: + return "Asuswrt Download Speed" + return None + + @property + def is_speed(self) -> bool: + """Return True if the type is an upload/download speed.""" + return self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED) + + @property + def is_size(self) -> bool: + """Return True if the type is the total upload/download size.""" + return self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the asuswrt sensors.""" if discovery_info is None: return - api = hass.data[DATA_ASUSWRT] + api: AsusWrt = hass.data[DATA_ASUSWRT] - devices = [] + # Let's discover the valid sensor types. + sensors = [_SensorTypes(x) for x in discovery_info] - if "devices" in discovery_info: - devices.append(AsuswrtDevicesSensor(api)) - if "download" in discovery_info: - devices.append(AsuswrtTotalRXSensor(api)) - if "upload" in discovery_info: - devices.append(AsuswrtTotalTXSensor(api)) - if "download_speed" in discovery_info: - devices.append(AsuswrtRXSensor(api)) - if "upload_speed" in discovery_info: - devices.append(AsuswrtTXSensor(api)) + data_handler = AsuswrtDataHandler(sensors, api) + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sensor", + update_method=data_handler.update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=30), + ) - add_entities(devices) + await coordinator.async_refresh() + async_add_entities([AsuswrtSensor(coordinator, x) for x in sensors]) -class AsuswrtSensor(Entity): - """Representation of a asuswrt sensor.""" +class AsuswrtDataHandler: + """Class handling the API updates.""" - _name = "generic" - - def __init__(self, api: AsusWrt): - """Initialize the sensor.""" + def __init__(self, sensors: List[_SensorTypes], api: AsusWrt): + """Initialize the handler class.""" self._api = api - self._state = None - self._devices = None - self._rates = None - self._speed = None - self._connect_error = False + self._sensors = sensors + self._connected = True - @property - def name(self): - """Return the name of the sensor.""" - return self._name + async def update_data(self) -> Dict[_SensorTypes, Any]: + """Fetch the relevant data from the router.""" + ret_dict: Dict[_SensorTypes, Any] = {} + try: + if _SensorTypes.DEVICES in self._sensors: + # Let's check the nr of devices. + devices = await self._api.async_get_connected_devices() + ret_dict[_SensorTypes.DEVICES] = len(devices) + + if any(x.is_speed for x in self._sensors): + # Let's check the upload and download speed + speed = await self._api.async_get_current_transfer_rates() + ret_dict[_SensorTypes.DOWNLOAD_SPEED] = round(speed[0] / 125000, 2) + ret_dict[_SensorTypes.UPLOAD_SPEED] = round(speed[1] / 125000, 2) + + if any(x.is_size for x in self._sensors): + rates = await self._api.async_get_bytes_total() + ret_dict[_SensorTypes.DOWNLOAD] = round(rates[0] / 1000000000, 1) + ret_dict[_SensorTypes.UPLOAD] = round(rates[1] / 1000000000, 1) + + if not self._connected: + # Log a successful reconnect + self._connected = True + _LOGGER.warning("Successfully reconnected to ASUS router") + + except OSError as err: + if self._connected: + # Log the first time connection was lost + _LOGGER.warning("Lost connection to router error due to: '%s'", err) + self._connected = False + + return ret_dict + + +class AsuswrtSensor(CoordinatorEntity): + """The asuswrt specific sensor class.""" + + def __init__(self, coordinator: DataUpdateCoordinator, sensor_type: _SensorTypes): + """Initialize the sensor class.""" + super().__init__(coordinator) + self._type = sensor_type @property def state(self): """Return the state of the sensor.""" - return self._state - - async def async_update(self): - """Fetch status from asuswrt.""" - try: - self._devices = await self._api.async_get_connected_devices() - self._rates = await self._api.async_get_bytes_total() - self._speed = await self._api.async_get_current_transfer_rates() - if self._connect_error: - self._connect_error = False - _LOGGER.info("Reconnected to ASUS router for %s update", self.entity_id) - except OSError as err: - if not self._connect_error: - self._connect_error = True - _LOGGER.error( - "Error connecting to ASUS router for %s update: %s", - self.entity_id, - err, - ) - - -class AsuswrtDevicesSensor(AsuswrtSensor): - """Representation of a asuswrt download speed sensor.""" - - _name = "Asuswrt Devices Connected" - - async def async_update(self): - """Fetch new state data for the sensor.""" - await super().async_update() - if self._devices: - self._state = len(self._devices) - - -class AsuswrtRXSensor(AsuswrtSensor): - """Representation of a asuswrt download speed sensor.""" - - _name = "Asuswrt Download Speed" - _unit = DATA_RATE_MEGABITS_PER_SECOND + return self.coordinator.data.get(self._type) @property - def icon(self): - """Return the icon.""" - return DOWNLOAD_ICON + def name(self) -> str: + """Return the name of the sensor.""" + return self._type.sensor_name @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - async def async_update(self): - """Fetch new state data for the sensor.""" - await super().async_update() - if self._speed: - self._state = round(self._speed[0] / 125000, 2) - - -class AsuswrtTXSensor(AsuswrtSensor): - """Representation of a asuswrt upload speed sensor.""" - - _name = "Asuswrt Upload Speed" - _unit = DATA_RATE_MEGABITS_PER_SECOND - - @property - def icon(self): - """Return the icon.""" - return UPLOAD_ICON - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - async def async_update(self): - """Fetch new state data for the sensor.""" - await super().async_update() - if self._speed: - self._state = round(self._speed[1] / 125000, 2) - - -class AsuswrtTotalRXSensor(AsuswrtSensor): - """Representation of a asuswrt total download sensor.""" - - _name = "Asuswrt Download" - _unit = DATA_GIGABYTES - - @property - def icon(self): - """Return the icon.""" - return DOWNLOAD_ICON - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - async def async_update(self): - """Fetch new state data for the sensor.""" - await super().async_update() - if self._rates: - self._state = round(self._rates[0] / 1000000000, 1) - - -class AsuswrtTotalTXSensor(AsuswrtSensor): - """Representation of a asuswrt total upload sensor.""" - - _name = "Asuswrt Upload" - _unit = DATA_GIGABYTES - - @property - def icon(self): - """Return the icon.""" - return UPLOAD_ICON - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - async def async_update(self): - """Fetch new state data for the sensor.""" - await super().async_update() - if self._rates: - self._state = round(self._rates[1] / 1000000000, 1) + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend.""" + return self._type.icon diff --git a/requirements_all.txt b/requirements_all.txt index 8fd652e7628..bce645f521c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ aio_georss_gdacs==0.4 aioambient==1.2.1 # homeassistant.components.asuswrt -aioasuswrt==1.3.0 +aioasuswrt==1.3.1 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdc982b1b23..3397842eb98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ aio_georss_gdacs==0.4 aioambient==1.2.1 # homeassistant.components.asuswrt -aioasuswrt==1.3.0 +aioasuswrt==1.3.1 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 6de3f1b2dcb..7c929992473 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,5 +1,4 @@ """The tests for the AsusWrt sensor platform.""" -from datetime import timedelta from aioasuswrt.asuswrt import Device @@ -16,10 +15,8 @@ from homeassistant.components.asuswrt import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from tests.async_mock import AsyncMock, patch -from tests.common import async_fire_time_changed VALID_CONFIG_ROUTER_SSH = { DOMAIN: { @@ -62,8 +59,6 @@ async def test_sensors(hass: HomeAssistant, mock_device_tracker_conf): assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_ROUTER_SSH) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() assert ( hass.states.get(f"{sensor.DOMAIN}.asuswrt_devices_connected").state == "3" From 755f15abe347d9f961b6547d98598645855e2571 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Nov 2020 12:40:25 +0100 Subject: [PATCH 215/430] Fix warning generated by surpetcare test (#43598) --- tests/components/surepetcare/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index 6d85a2b0189..ed42fe2532b 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -7,8 +7,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from tests.async_mock import AsyncMock, patch -@fixture() -def surepetcare(hass): +@fixture +async def surepetcare(hass): """Mock the SurePetcare for easier testing.""" with patch("homeassistant.components.surepetcare.SurePetcare") as mock_surepetcare: instance = mock_surepetcare.return_value = SurePetcare( From 7214d6517ac05b39470581acac09eb60c3374b5b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 24 Nov 2020 14:47:56 +0100 Subject: [PATCH 216/430] Fix conversion of cover position between HASS and deCONZ (#43602) --- homeassistant/components/deconz/cover.py | 3 ++- tests/components/deconz/test_cover.py | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index ab5f6e0be9e..2d191b7f7ca 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -85,7 +85,8 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - await self._device.set_position(kwargs[ATTR_POSITION]) + position = 100 - kwargs[ATTR_POSITION] + await self._device.set_position(position) async def async_open_cover(self, **kwargs): """Open cover.""" diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 3e31006438a..089a3d90a43 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -47,7 +47,7 @@ COVERS = { "id": "deconz old brightness cover id", "name": "deconz old brightness cover", "type": "Level controllable output", - "state": {"bri": 255, "on": False, "reachable": True}, + "state": {"bri": 254, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:03-00", }, @@ -55,7 +55,7 @@ COVERS = { "id": "Window covering controller id", "name": "Window covering controller", "type": "Window covering controller", - "state": {"bri": 254, "on": True, "reachable": True}, + "state": {"bri": 253, "on": True, "reachable": True}, "modelid": "Motor controller", "uniqueid": "00:00:00:00:00:00:00:04-00", }, @@ -147,11 +147,11 @@ async def test_cover(hass): await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 40}, blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 50}) + set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 60}) # Service stop cover movement @@ -207,11 +207,11 @@ async def test_cover(hass): await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40}, blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 127}) + set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 152}) # Service stop cover movement From 3dd14e05e336153a5d7310df0e425e79b2580543 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 24 Nov 2020 07:53:50 -0800 Subject: [PATCH 217/430] Update nest library and switch events to async (#43583) --- homeassistant/components/nest/__init__.py | 6 +++--- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/climate_sdm_test.py | 8 ++++---- tests/components/nest/common.py | 10 +++++----- tests/components/nest/sensor_sdm_test.py | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index c70184c357b..7f3e9576bf4 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta import logging import threading -from google_nest_sdm.event import EventCallback, EventMessage +from google_nest_sdm.event import AsyncEventCallback, EventMessage from google_nest_sdm.exceptions import GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from nest import Nest @@ -160,14 +160,14 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -class SignalUpdateCallback(EventCallback): +class SignalUpdateCallback(AsyncEventCallback): """An EventCallback invoked when new events arrive from subscriber.""" def __init__(self, hass: HomeAssistant): """Initialize EventCallback.""" self._hass = hass - def handle_event(self, event_message: EventMessage): + async def async_handle_event(self, event_message: EventMessage): """Process an incoming EventMessage.""" _LOGGER.debug("Update %s @ %s", event_message.event_id, event_message.timestamp) traits = event_message.resource_update_traits diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index b994fcfbfce..60293612cd3 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0", - "google-nest-sdm==0.1.15" + "google-nest-sdm==0.2.0" ], "codeowners": [ "@awarecan", diff --git a/requirements_all.txt b/requirements_all.txt index bce645f521c..ff2c7c22cb3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -684,7 +684,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.1.15 +google-nest-sdm==0.2.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3397842eb98..b28ee9e489d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.1.15 +google-nest-sdm==0.2.0 # homeassistant.components.gree greeclimate==0.10.3 diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index 4c7ec4c0163..bf6716ec966 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -417,7 +417,7 @@ async def test_thermostat_set_hvac_mode(hass, auth): }, auth=None, ) - subscriber.receive_event(event) + await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal thermostat = hass.states.get("climate.my_thermostat") @@ -441,7 +441,7 @@ async def test_thermostat_set_hvac_mode(hass, auth): }, auth=None, ) - subscriber.receive_event(event) + await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal thermostat = hass.states.get("climate.my_thermostat") @@ -514,7 +514,7 @@ async def test_thermostat_set_eco_preset(hass, auth): }, auth=auth, ) - subscriber.receive_event(event) + await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal thermostat = hass.states.get("climate.my_thermostat") @@ -834,7 +834,7 @@ async def test_thermostat_target_temp(hass, auth): }, auth=None, ) - subscriber.receive_event(event) + await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal thermostat = hass.states.get("climate.my_thermostat") diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index c1c8dbd04d7..cd3a06a5afa 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -3,7 +3,7 @@ import time from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.event import EventCallback, EventMessage +from google_nest_sdm.event import AsyncEventCallback, EventMessage from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN @@ -61,7 +61,7 @@ class FakeSubscriber(GoogleNestSubscriber): self._device_manager = device_manager self._callback = None - def set_update_callback(self, callback: EventCallback): + def set_update_callback(self, callback: AsyncEventCallback): """Capture the callback set by Home Assistant.""" self._callback = callback @@ -77,11 +77,11 @@ class FakeSubscriber(GoogleNestSubscriber): """No-op to stop the subscriber.""" return None - def receive_event(self, event_message: EventMessage): + async def async_receive_event(self, event_message: EventMessage): """Simulate a received pubsub message, invoked by tests.""" # Update device state, then invoke HomeAssistant to refresh - self._device_manager.handle_event(event_message) - self._callback.handle_event(event_message) + await self._device_manager.async_handle_event(event_message) + await self._callback.async_handle_event(event_message) async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index 7d2e299a1a1..b8b2912b124 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -164,7 +164,7 @@ async def test_event_updates_sensor(hass): }, auth=None, ) - subscriber.receive_event(event) + await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal temperature = hass.states.get("sensor.my_sensor_temperature") From 5a892e8fca6ef3503e77a6bbbf55175c3654c9fd Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:00:16 +0100 Subject: [PATCH 218/430] Add unknown_authorize_url_generation to base strings for config flows (#42484) --- homeassistant/components/nest/config_flow.py | 2 +- homeassistant/components/nest/strings.json | 2 +- homeassistant/components/point/config_flow.py | 2 +- homeassistant/components/point/strings.json | 2 +- homeassistant/components/tellduslive/config_flow.py | 4 ++-- homeassistant/components/tellduslive/strings.json | 2 +- homeassistant/components/toon/strings.json | 2 +- homeassistant/strings.json | 3 ++- tests/components/nest/test_config_flow_legacy.py | 2 +- tests/components/point/test_config_flow.py | 2 +- tests/components/tellduslive/test_config_flow.py | 4 ++-- 11 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index e792b496da5..6aaa5bcc489 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -177,7 +177,7 @@ class NestFlowHandler( return self.async_abort(reason="authorize_url_timeout") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error generating auth url") - return self.async_abort(reason="authorize_url_fail") + return self.async_abort(reason="unknown_authorize_url_generation") return self.async_show_form( step_id="link", diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 6882e8f55e7..0ce9c902121 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -25,7 +25,7 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "authorize_url_fail": "Unknown error generating an authorize url.", + "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 815b872d3e9..aaefc45bc9c 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -101,7 +101,7 @@ class PointFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="authorize_url_timeout") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error generating auth url") - return self.async_abort(reason="authorize_url_fail") + return self.async_abort(reason="unknown_authorize_url_generation") return self.async_show_form( step_id="auth", description_placeholders={"authorization_url": url}, diff --git a/homeassistant/components/point/strings.json b/homeassistant/components/point/strings.json index 194121e8e25..8a28e314b69 100644 --- a/homeassistant/components/point/strings.json +++ b/homeassistant/components/point/strings.json @@ -23,7 +23,7 @@ "external_setup": "Point successfully configured from another flow.", "no_flows": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "authorize_url_fail": "Unknown error generating an authorize url." + "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]" } } } diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index ae8a9d1a690..aabbf88ee1c 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -97,12 +97,12 @@ class FlowHandler(config_entries.ConfigFlow): with async_timeout.timeout(10): auth_url = await self.hass.async_add_executor_job(self._get_auth_url) if not auth_url: - return self.async_abort(reason="authorize_url_fail") + return self.async_abort(reason="unknown_authorize_url_generation") except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error generating auth url") - return self.async_abort(reason="authorize_url_fail") + return self.async_abort(reason="unknown_authorize_url_generation") _LOGGER.debug("Got authorization URL %s", auth_url) return self.async_show_form( diff --git a/homeassistant/components/tellduslive/strings.json b/homeassistant/components/tellduslive/strings.json index 8d1c2c1acaf..27e74d6d938 100644 --- a/homeassistant/components/tellduslive/strings.json +++ b/homeassistant/components/tellduslive/strings.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "authorize_url_fail": "Unknown error generating an authorize url.", + "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index c5ac07516b6..60d5ed3312c 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -14,7 +14,7 @@ }, "abort": { "already_configured": "The selected agreement is already configured.", - "authorize_url_fail": "Unknown error generating an authorize url.", + "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_agreements": "This account has no Toon displays.", diff --git a/homeassistant/strings.json b/homeassistant/strings.json index f36c62b91ce..e2a85637fbb 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -70,7 +70,8 @@ "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "unknown_authorize_url_generation": "Unknown error generating an authorize url." } } } diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index e0e93bf626a..23e01cf239a 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -100,7 +100,7 @@ async def test_abort_if_exception_generating_auth_url(hass): flow.hass = hass result = await flow.async_step_init() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "authorize_url_fail" + assert result["reason"] == "unknown_authorize_url_generation" async def test_verify_code_timeout(hass): diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index b6c780e937a..67817b308ce 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -141,7 +141,7 @@ async def test_abort_if_exception_generating_auth_url(hass): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "authorize_url_fail" + assert result["reason"] == "unknown_authorize_url_generation" async def test_abort_no_code(hass): diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 66476c8735d..7417c87c229 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -229,7 +229,7 @@ async def test_abort_no_auth_url(hass, mock_tellduslive): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "authorize_url_fail" + assert result["reason"] == "unknown_authorize_url_generation" async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): @@ -238,7 +238,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "authorize_url_fail" + assert result["reason"] == "unknown_authorize_url_generation" async def test_discovery_already_configured(hass, mock_tellduslive): From d493f18e50ec257908ac925f1533f9943a72e25d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 24 Nov 2020 19:55:15 +0100 Subject: [PATCH 219/430] Bump hatasmota to 0.1.1 (#43608) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 88 ++++++++++++++++++- 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 6140de6025a..076371c9792 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.0.32"], + "requirements": ["hatasmota==0.1.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index ff2c7c22cb3..bc6137672c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.0.32 +hatasmota==0.1.1 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b28ee9e489d..38366a499b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.0.32 +hatasmota==0.1.1 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 9210c577a5e..7bfa4ac6d99 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -674,11 +674,11 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # Dim the light from 0->50: Speed should be 4*2/2=4 + # Dim the light from 0->50: Speed should be 4*2*2=16 await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 4;NoDelay;Dimmer 50", + "NoDelay;Fade 1;NoDelay;Speed 16;NoDelay;Dimmer 50", 0, False, ) @@ -692,11 +692,91 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 - # Dim the light from 50->0: Speed should be 6*2/2=6 + # Dim the light from 50->0: Speed should be 6*2*2=24 await common.async_turn_off(hass, "light.test", transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 6;NoDelay;Power1 OFF", + "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Fake state update from the light + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":50, "Color":"0,255,0"}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("rgb_color") == (0, 255, 0) + + # Set color of the light from 0,255,0 to 255,0,0 @ 50%: Speed should be 6*2*2=24 + await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Fake state update from the light + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":100, "Color":"0,255,0"}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("rgb_color") == (0, 255, 0) + + # Set color of the light from 0,255,0 to 255,0,0 @ 100%: Speed should be 6*2=12 + await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Fake state update from the light + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":153}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_temp") == 153 + + # Set color_temp of the light from 153 to 500 @ 50%: Speed should be 6*2*2=24 + await common.async_turn_on(hass, "light.test", color_temp=500, transition=6) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;CT 500", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Fake state update from the light + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":500}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_temp") == 500 + + # Set color_temp of the light from 500 to 326 @ 50%: Speed should be 6*2*2*2=48->40 + await common.async_turn_on(hass, "light.test", color_temp=326, transition=6) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Power1 ON;NoDelay;CT 326", 0, False, ) From aed6980b1430028a4f25059c4507949e7125c24c Mon Sep 17 00:00:00 2001 From: koolsb <14332595+koolsb@users.noreply.github.com> Date: Tue, 24 Nov 2020 12:56:39 -0600 Subject: [PATCH 220/430] Fix Meraki API Auth (#43578) --- homeassistant/components/meraki/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 614c2943530..55186d63146 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -36,6 +36,7 @@ class MerakiView(HomeAssistantView): url = URL name = "api:meraki" + requires_auth = False def __init__(self, config, async_see): """Initialize Meraki URL endpoints.""" From dae286aed5fc564ac5a4b22d7aee695d69fe1cd4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Nov 2020 21:25:23 +0100 Subject: [PATCH 221/430] Upgrade foobot_async to 1.0.0 (#43611) --- homeassistant/components/foobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index f4a4315055d..09458a18d91 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -2,6 +2,6 @@ "domain": "foobot", "name": "Foobot", "documentation": "https://www.home-assistant.io/integrations/foobot", - "requirements": ["foobot_async==0.3.2"], + "requirements": ["foobot_async==1.0.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index bc6137672c5..c85630f7c50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -608,7 +608,7 @@ flux_led==0.22 fnvhash==0.1.0 # homeassistant.components.foobot -foobot_async==0.3.2 +foobot_async==1.0.0 # homeassistant.components.fortios fortiosapi==0.10.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38366a499b8..a7bc7f10ab2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -305,7 +305,7 @@ feedparser-homeassistant==5.2.2.dev1 fnvhash==0.1.0 # homeassistant.components.foobot -foobot_async==0.3.2 +foobot_async==1.0.0 # homeassistant.components.google_translate gTTS-token==1.1.4 From acb94b0b596ce0708d77295932c7e6b104d9c2fe Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 24 Nov 2020 21:42:11 +0100 Subject: [PATCH 222/430] Add tilt support to deCONZ covers (#43607) --- homeassistant/components/deconz/cover.py | 56 +++++++++--- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_cover.py | 87 ++++++++++++++++++- 5 files changed, 133 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 2d191b7f7ca..48218cf893a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,12 +1,17 @@ """Support for deCONZ covers.""" from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DEVICE_CLASS_WINDOW, DOMAIN, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + SUPPORT_STOP_TILT, CoverEntity, ) from homeassistant.core import callback @@ -60,15 +65,16 @@ class DeconzCover(DeconzDevice, CoverEntity): self._features |= SUPPORT_STOP self._features |= SUPPORT_SET_POSITION - @property - def current_cover_position(self): - """Return the current position of the cover.""" - return 100 - self._device.position + if self._device.tilt is not None: + self._features |= SUPPORT_OPEN_TILT + self._features |= SUPPORT_CLOSE_TILT + self._features |= SUPPORT_STOP_TILT + self._features |= SUPPORT_SET_TILT_POSITION @property - def is_closed(self): - """Return if the cover is closed.""" - return not self._device.is_open + def supported_features(self): + """Flag supported features.""" + return self._features @property def device_class(self): @@ -79,14 +85,19 @@ class DeconzCover(DeconzDevice, CoverEntity): return DEVICE_CLASS_WINDOW @property - def supported_features(self): - """Flag supported features.""" - return self._features + def current_cover_position(self): + """Return the current position of the cover.""" + return 100 - self._device.lift + + @property + def is_closed(self): + """Return if the cover is closed.""" + return not self._device.is_open async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = 100 - kwargs[ATTR_POSITION] - await self._device.set_position(position) + await self._device.set_position(lift=position) async def async_open_cover(self, **kwargs): """Open cover.""" @@ -99,3 +110,26 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop cover.""" await self._device.stop() + + @property + def current_cover_tilt_position(self): + """Return the current tilt position of the cover.""" + if self._device.tilt is not None: + return 100 - self._device.tilt + + async def async_set_cover_tilt_position(self, **kwargs): + """Tilt the cover to a specific position.""" + position = 100 - kwargs[ATTR_TILT_POSITION] + await self._device.set_position(tilt=position) + + async def async_open_cover_tilt(self, **kwargs): + """Open cover tilt.""" + await self._device.set_position(tilt=0) + + async def async_close_cover_tilt(self, **kwargs): + """Close cover tilt.""" + await self._device.set_position(tilt=100) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + await self._device.stop() diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 7b8abe82472..e9b388e29fe 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==74"], + "requirements": ["pydeconz==75"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index c85630f7c50..5e2d832f247 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1337,7 +1337,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==74 +pydeconz==75 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7bc7f10ab2..826229436af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -670,7 +670,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==74 +pydeconz==75 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 089a3d90a43..374d3683a6e 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -3,12 +3,18 @@ from copy import deepcopy from homeassistant.components.cover import ( + ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, 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_STOP_COVER_TILT, ) from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -47,7 +53,7 @@ COVERS = { "id": "deconz old brightness cover id", "name": "deconz old brightness cover", "type": "Level controllable output", - "state": {"bri": 254, "on": False, "reachable": True}, + "state": {"bri": 255, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:03-00", }, @@ -165,7 +171,7 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/2/state", json={"bri_inc": 0}) + set_callback.assert_called_with("put", "/lights/2/state", json={"stop": True}) # Verify service calls for legacy cover @@ -247,3 +253,80 @@ async def test_cover(hass): await hass.config_entries.async_unload(config_entry.entry_id) assert len(hass.states.async_all()) == 0 + + +async def test_tilt_cover(hass): + """Test that tilting a cover works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = { + "0": { + "etag": "87269755b9b3a046485fdae8d96b252c", + "lastannounced": None, + "lastseen": "2020-08-01T16:22:05Z", + "manufacturername": "AXIS", + "modelid": "Gear", + "name": "Covering device", + "state": { + "bri": 0, + "lift": 0, + "on": False, + "open": True, + "reachable": True, + "tilt": 0, + }, + "swversion": "100-5.3.5.1122", + "type": "Window covering device", + "uniqueid": "00:24:46:00:00:12:34:56-01", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 1 + entity = hass.states.get("cover.covering_device") + assert entity.state == STATE_OPEN + assert entity.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + covering_device = gateway.api.lights["0"] + + with patch.object(covering_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.covering_device", ATTR_TILT_POSITION: 40}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 60}) + + with patch.object(covering_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.covering_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 0}) + + with patch.object(covering_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: "cover.covering_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 100}) + + # Service stop cover movement + + with patch.object(covering_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: "cover.covering_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/0/state", json={"stop": True}) From 48d9f1a61b8f112d0c1e76320185420a77f08c74 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Nov 2020 21:44:31 +0100 Subject: [PATCH 223/430] Fix duplicate check on onewire config flow (#43590) --- .../components/onewire/config_flow.py | 2 +- tests/components/onewire/__init__.py | 6 ++-- tests/components/onewire/test_config_flow.py | 33 +++++++++++++++++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index f83431111d8..9ad4d5347f0 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -56,7 +56,7 @@ def is_duplicate_owserver_entry(hass: HomeAssistantType, user_input): if ( config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER and config_entry.data[CONF_HOST] == user_input[CONF_HOST] - and config_entry.data[CONF_PORT] == str(user_input[CONF_PORT]) + and config_entry.data[CONF_PORT] == user_input[CONF_PORT] ): return True return False diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index eb9b42ea996..39a3c438cf9 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -48,9 +48,8 @@ async def setup_onewire_owserver_integration(hass): data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", - CONF_PORT: "1234", + CONF_PORT: 1234, }, - unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", @@ -74,12 +73,11 @@ async def setup_onewire_patched_owserver_integration(hass): data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", - CONF_PORT: "1234", + CONF_PORT: 1234, CONF_NAMES: { "10.111111111111": "My DS18B20", }, }, - unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index dfb64a3846e..ba0ae090ed2 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -318,7 +318,7 @@ async def test_import_owserver_with_port(hass): data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", - CONF_PORT: "1234", + CONF_PORT: 1234, }, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY @@ -326,8 +326,37 @@ async def test_import_owserver_with_port(hass): assert result["data"] == { CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", - CONF_PORT: "1234", + CONF_PORT: 1234, } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_owserver_duplicate(hass): + """Test OWServer flow.""" + # Initialise with single entry + with patch( + "homeassistant.components.onewire.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.onewire.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await setup_onewire_owserver_integration(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + # Import duplicate entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_TYPE: CONF_TYPE_OWSERVER, + CONF_HOST: "1.2.3.4", + CONF_PORT: 1234, + }, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 7f9a7791bff357ea0f6e272d9d415a6209c21208 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 24 Nov 2020 22:58:17 +0100 Subject: [PATCH 224/430] update xknx to 0.15.4 (#43536) --- homeassistant/components/knx/cover.py | 3 +++ homeassistant/components/knx/factory.py | 1 + homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 1 + requirements_all.txt | 2 +- 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index c677b12c0ee..b88b1cfe86a 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -5,6 +5,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DEVICE_CLASS_BLIND, + DEVICE_CLASSES, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -47,6 +48,8 @@ class KNXCover(KnxEntity, CoverEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" + if self._device.device_class in DEVICE_CLASSES: + return self._device.device_class if self._device.supports_angle: return DEVICE_CLASS_BLIND return None diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 6da2b73cd6a..385b7c009ed 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -82,6 +82,7 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP], invert_position=config[CoverSchema.CONF_INVERT_POSITION], invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], + device_class=config.get(CONF_DEVICE_CLASS), ) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4055048fd2d..6b253c2a010 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.15.3"], + "requirements": ["xknx==0.15.4"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index cbf06925163..c17667cbed2 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -77,6 +77,7 @@ class CoverSchema: ): cv.positive_int, vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): cv.string, } ) diff --git a/requirements_all.txt b/requirements_all.txt index 5e2d832f247..bd8f81da42f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2307,7 +2307,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.15.3 +xknx==0.15.4 # homeassistant.components.bluesound # homeassistant.components.rest From 745823dd5520ce0bbbb1a3a42d07ffdf3b1edaca Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 24 Nov 2020 14:34:43 -0800 Subject: [PATCH 225/430] Add nest SDM API camera/doorbell events (#42700) * Add nest SDM API camera/doorbell events Events are fired when pubsub messages are received. When messages are received lookup a home assistant device id from the nest device id, so that the home assistant device id can be included in the event payload. * Update homeassistant/components/nest/__init__.py Co-authored-by: Paulus Schoutsen * Update nest code style based on PR feedback Co-authored-by: Paulus Schoutsen --- homeassistant/components/nest/__init__.py | 51 ++++- homeassistant/components/nest/sensor_sdm.py | 2 - tests/components/nest/test_events.py | 237 ++++++++++++++++++++ 3 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 tests/components/nest/test_events.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 7f3e9576bf4..7c2564f01bb 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -5,7 +5,14 @@ from datetime import datetime, timedelta import logging import threading -from google_nest_sdm.event import AsyncEventCallback, EventMessage +from google_nest_sdm.event import ( + AsyncEventCallback, + CameraMotionEvent, + CameraPersonEvent, + CameraSoundEvent, + DoorbellChimeEvent, + EventMessage, +) from google_nest_sdm.exceptions import GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from nest import Nest @@ -54,6 +61,14 @@ _LOGGER = logging.getLogger(__name__) CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" +NEST_EVENT = "nest_event" +EVENT_TRAIT_MAP = { + DoorbellChimeEvent.NAME: "DoorbellChime", + CameraMotionEvent.NAME: "CameraMotion", + CameraPersonEvent.NAME: "CameraPerson", + CameraSoundEvent.NAME: "CameraSound", +} + # Configuration for the legacy nest API SERVICE_CANCEL_ETA = "cancel_eta" @@ -169,21 +184,35 @@ class SignalUpdateCallback(AsyncEventCallback): async def async_handle_event(self, event_message: EventMessage): """Process an incoming EventMessage.""" - _LOGGER.debug("Update %s @ %s", event_message.event_id, event_message.timestamp) + if not event_message.resource_update_name: + _LOGGER.debug("Ignoring event with no device_id") + return + device_id = event_message.resource_update_name + _LOGGER.debug("Update for %s @ %s", device_id, event_message.timestamp) traits = event_message.resource_update_traits if traits: _LOGGER.debug("Trait update %s", traits.keys()) + # This event triggered an update to a device that changed some + # properties which the DeviceManager should already have received. + # Send a signal to refresh state of all listening devices. + async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE) events = event_message.resource_update_events - if events: - _LOGGER.debug("Event Update %s", events.keys()) - - if not event_message.resource_update_traits: - # Note: Currently ignoring events like camera motion + if not events: return - # This event triggered an update to a device that changed some - # properties which the DeviceManager should already have received. - # Send a signal to refresh state of all listening devices. - async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE) + _LOGGER.debug("Event Update %s", events.keys()) + device_registry = await self._hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_device({(DOMAIN, device_id)}, ()) + if not device_entry: + _LOGGER.debug("Ignoring event for unregistered device '%s'", device_id) + return + for event in events: + if event not in EVENT_TRAIT_MAP: + continue + message = { + "device_id": device_entry.id, + "type": EVENT_TRAIT_MAP[event], + } + self._hass.bus.async_fire(NEST_EVENT, message) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index a0a28756ac2..b2b9500a156 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -45,8 +45,6 @@ async def async_setup_sdm_entry( _LOGGER.warning("Failed to get devices: %s", err) raise PlatformNotReady from err - # Fetch initial data so we have data when entities subscribe. - entities = [] for device in device_manager.devices.values(): if TemperatureTrait.NAME in device.traits: diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py new file mode 100644 index 00000000000..b4b670fefbf --- /dev/null +++ b/tests/components/nest/test_events.py @@ -0,0 +1,237 @@ +"""Test for Nest binary sensor platform for the Smart Device Management API. + +These tests fake out the subscriber/devicemanager, and are not using a real +pubsub subscriber. +""" + +from google_nest_sdm.device import Device +from google_nest_sdm.event import EventMessage + +from homeassistant.util.dt import utcnow + +from .common import async_setup_sdm_platform + +from tests.common import async_capture_events + +DOMAIN = "nest" +DEVICE_ID = "some-device-id" +PLATFORM = "camera" +NEST_EVENT = "nest_event" +EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." +EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + + +async def async_setup_devices(hass, device_type, traits={}): + """Set up the platform and prerequisites.""" + devices = { + DEVICE_ID: Device.MakeDevice( + { + "name": DEVICE_ID, + "type": device_type, + "traits": traits, + }, + auth=None, + ), + } + return await async_setup_sdm_platform(hass, PLATFORM, devices=devices) + + +def create_device_traits(event_trait): + """Create fake traits for a device.""" + return { + "sdm.devices.traits.Info": { + "customName": "Front", + }, + event_trait: {}, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + }, + } + + +def create_event(event_type, device_id=DEVICE_ID): + """Create an EventMessage for a single event type.""" + events = { + event_type: { + "eventSessionId": EVENT_SESSION_ID, + "eventId": EVENT_ID, + }, + } + return create_events(events=events, device_id=device_id) + + +def create_events(events, device_id=DEVICE_ID): + """Create an EventMessage for events.""" + return EventMessage( + { + "eventId": "some-event-id", + "timestamp": utcnow().isoformat(timespec="seconds"), + "resourceUpdate": { + "name": device_id, + "events": events, + }, + }, + auth=None, + ) + + +async def test_doorbell_chime_event(hass): + """Test a pubsub message for a doorbell event.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits("sdm.devices.traits.DoorbellChime"), + ) + + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("camera.front") + assert entry is not None + assert entry.unique_id == "some-device-id-camera" + assert entry.original_name == "Front" + assert entry.domain == "camera" + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + assert device.name == "Front" + assert device.model == "Doorbell" + assert device.identifiers == {("nest", DEVICE_ID)} + + await subscriber.async_receive_event( + create_event("sdm.devices.events.DoorbellChime.Chime") + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + "device_id": entry.device_id, + "type": "DoorbellChime", + } + + +async def test_camera_motion_event(hass): + """Test a pubsub message for a camera motion event.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.CAMERA", + create_device_traits("sdm.devices.traits.CameraMotion"), + ) + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("camera.front") + assert entry is not None + + await subscriber.async_receive_event( + create_event("sdm.devices.events.CameraMotion.Motion") + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + "device_id": entry.device_id, + "type": "CameraMotion", + } + + +async def test_camera_sound_event(hass): + """Test a pubsub message for a camera sound event.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.CAMERA", + create_device_traits("sdm.devices.traits.CameraSound"), + ) + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("camera.front") + assert entry is not None + + await subscriber.async_receive_event( + create_event("sdm.devices.events.CameraSound.Sound") + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + "device_id": entry.device_id, + "type": "CameraSound", + } + + +async def test_camera_person_event(hass): + """Test a pubsub message for a camera person event.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits("sdm.devices.traits.CameraEventImage"), + ) + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("camera.front") + assert entry is not None + + await subscriber.async_receive_event( + create_event("sdm.devices.events.CameraPerson.Person") + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + "device_id": entry.device_id, + "type": "CameraPerson", + } + + +async def test_camera_multiple_event(hass): + """Test a pubsub message for a camera person event.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits("sdm.devices.traits.CameraEventImage"), + ) + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("camera.front") + assert entry is not None + + event_map = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": EVENT_ID, + }, + "sdm.devices.events.CameraPerson.Person": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": EVENT_ID, + }, + } + + await subscriber.async_receive_event(create_events(event_map)) + await hass.async_block_till_done() + + assert len(events) == 2 + assert events[0].data == { + "device_id": entry.device_id, + "type": "CameraMotion", + } + assert events[1].data == { + "device_id": entry.device_id, + "type": "CameraPerson", + } + + +async def test_unknown_event(hass): + """Test a pubsub message for an unknown event type.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits("sdm.devices.traits.DoorbellChime"), + ) + await subscriber.async_receive_event(create_event("some-event-id")) + await hass.async_block_till_done() + + assert len(events) == 0 From cbfd8a5a141cf03b7e7cd8973d65c4ef977a8c6b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 25 Nov 2020 00:03:05 +0000 Subject: [PATCH 226/430] [ci skip] Translation update --- .../components/kodi/translations/es.json | 1 + .../components/kodi/translations/it.json | 1 + .../components/local_ip/translations/it.json | 1 + .../motion_blinds/translations/ca.json | 20 ++++++++++++++++ .../motion_blinds/translations/cs.json | 20 ++++++++++++++++ .../motion_blinds/translations/es.json | 20 ++++++++++++++++ .../motion_blinds/translations/et.json | 20 ++++++++++++++++ .../motion_blinds/translations/it.json | 20 ++++++++++++++++ .../motion_blinds/translations/no.json | 20 ++++++++++++++++ .../motion_blinds/translations/ru.json | 20 ++++++++++++++++ .../motion_blinds/translations/zh-Hant.json | 20 ++++++++++++++++ .../components/nest/translations/ca.json | 3 ++- .../components/nest/translations/en.json | 3 ++- .../components/nest/translations/pl.json | 5 ++-- .../components/nest/translations/ru.json | 3 ++- .../nightscout/translations/pl.json | 2 +- .../ovo_energy/translations/it.json | 8 +++++++ .../components/point/translations/ca.json | 3 ++- .../components/point/translations/en.json | 3 ++- .../components/point/translations/pl.json | 5 ++-- .../components/point/translations/ru.json | 3 ++- .../components/solaredge/translations/ca.json | 7 +++++- .../components/solaredge/translations/cs.json | 4 ++++ .../components/solaredge/translations/es.json | 7 +++++- .../components/solaredge/translations/et.json | 7 +++++- .../components/solaredge/translations/it.json | 7 +++++- .../components/solaredge/translations/no.json | 7 +++++- .../components/solaredge/translations/ru.json | 7 +++++- .../solaredge/translations/zh-Hant.json | 7 +++++- .../srp_energy/translations/it.json | 24 +++++++++++++++++++ .../tellduslive/translations/ca.json | 3 ++- .../tellduslive/translations/en.json | 3 ++- .../tellduslive/translations/pl.json | 5 ++-- .../tellduslive/translations/ru.json | 3 ++- .../components/toon/translations/ca.json | 3 ++- .../components/toon/translations/en.json | 3 ++- .../components/toon/translations/pl.json | 5 ++-- .../components/toon/translations/ru.json | 3 ++- 38 files changed, 278 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/motion_blinds/translations/ca.json create mode 100644 homeassistant/components/motion_blinds/translations/cs.json create mode 100644 homeassistant/components/motion_blinds/translations/es.json create mode 100644 homeassistant/components/motion_blinds/translations/et.json create mode 100644 homeassistant/components/motion_blinds/translations/it.json create mode 100644 homeassistant/components/motion_blinds/translations/no.json create mode 100644 homeassistant/components/motion_blinds/translations/ru.json create mode 100644 homeassistant/components/motion_blinds/translations/zh-Hant.json create mode 100644 homeassistant/components/srp_energy/translations/it.json diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index ddea2a65a2f..b19d09a92d0 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -4,6 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autentificacion invalida", + "no_uuid": "La instancia de Kodi no tiene un identificador \u00fanico. Esto probablemente es debido a una versi\u00f3n antigua Kodi (17.x o inferior). Puedes configurar la integraci\u00f3n manualmente o actualizar a una versi\u00f3n m\u00e1s reciente de Kodi.", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/kodi/translations/it.json b/homeassistant/components/kodi/translations/it.json index ad783a26a6b..cadec0d387e 100644 --- a/homeassistant/components/kodi/translations/it.json +++ b/homeassistant/components/kodi/translations/it.json @@ -4,6 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "no_uuid": "L'istanza di Kodi non ha un ID univoco. Ci\u00f2 \u00e8 molto probabilmente dovuto a una vecchia versione di Kodi (17.xo inferiore). Puoi configurare l'integrazione manualmente o eseguire l'aggiornamento a una versione di Kodi pi\u00f9 recente.", "unknown": "Errore imprevisto" }, "error": { diff --git a/homeassistant/components/local_ip/translations/it.json b/homeassistant/components/local_ip/translations/it.json index 9173584c9f8..db47d7b9f9f 100644 --- a/homeassistant/components/local_ip/translations/it.json +++ b/homeassistant/components/local_ip/translations/it.json @@ -8,6 +8,7 @@ "data": { "name": "Nome del sensore" }, + "description": "Vuoi iniziare la configurazione?", "title": "Indirizzo IP locale" } } diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json new file mode 100644 index 00000000000..a4bf96457e6 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "connection_error": "Ha fallat la connexi\u00f3" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "Clau API", + "host": "Adre\u00e7a IP" + }, + "description": "Necessitar\u00e0s el token d'API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/cs.json b/homeassistant/components/motion_blinds/translations/cs.json new file mode 100644 index 00000000000..41b5db3c83e --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "host": "IP adresa" + }, + "description": "Budete pot\u0159ebovat 16m\u00edstn\u00fd API kl\u00ed\u010d, pokyny najdete na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json new file mode 100644 index 00000000000..bac5ffddbd3 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "connection_error": "No se pudo conectar" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "Clave API", + "host": "Direcci\u00f3n IP" + }, + "description": "Necesitar\u00e1s la Clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para instrucciones", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json new file mode 100644 index 00000000000..b55640d8905 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "connection_error": "\u00dchendamine nurjus" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "host": "IP-aadress" + }, + "description": "Vaja on 16-kohalist API-v\u00f5tit. Juhiste saamiseks vt https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json new file mode 100644 index 00000000000..ff56f184ac2 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "connection_error": "Impossibile connettersi" + }, + "flow_title": "Tende Motion", + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "host": "Indirizzo IP" + }, + "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", + "title": "Tende Motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json new file mode 100644 index 00000000000..9e406150691 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "connection_error": "Tilkobling mislyktes" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "host": "IP adresse" + }, + "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json new file mode 100644 index 00000000000..1a249a4fab8 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json new file mode 100644 index 00000000000..8c8d23b565b --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "connection_error": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "host": "IP \u4f4d\u5740" + }, + "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index e65e264e3f7..46558f2e89a 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", + "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 8839f24e30a..3df0a9fa76d 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown_authorize_url_generation": "Unknown error generating an authorize url." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 77c86ddfb07..99066e26fd8 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji", + "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", + "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index be8905156bd..d18304c9f50 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -5,7 +5,8 @@ "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \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 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", + "unknown_authorize_url_generation": "\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." }, "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/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json index a21e509eec7..a544fc887c0 100644 --- a/homeassistant/components/nightscout/translations/pl.json +++ b/homeassistant/components/nightscout/translations/pl.json @@ -15,7 +15,7 @@ "api_key": "Klucz API", "url": "URL" }, - "description": "- URL: adres url Twojej instancji Nightscout. Np: https://myhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalny): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).", + "description": "- URL: adres URL Twojej instancji Nightscout. Np: https://m\u00f3jhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalny): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).", "title": "Wprowad\u017a informacje o serwerze Nightscout." } } diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json index f0619e7d593..9f4c2c68935 100644 --- a/homeassistant/components/ovo_energy/translations/it.json +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -5,7 +5,15 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, + "flow_title": "OVO Energy: {username}", "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "Autenticazione non riuscita per OVO Energy. Immettere le credenziali correnti.", + "title": "Riautenticazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index b0814e56615..158c6addade 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -5,7 +5,8 @@ "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.", - "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index cd6731a4bb0..50e9e4f3ce0 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -5,7 +5,8 @@ "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": "The component is not configured. Please follow the documentation." + "no_flows": "The component is not configured. Please follow the documentation.", + "unknown_authorize_url_generation": "Unknown error generating an authorize url." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json index e0624b5ff98..66b81d5675e 100644 --- a/homeassistant/components/point/translations/pl.json +++ b/homeassistant/components/point/translations/pl.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji", + "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": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "no_flows": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json index a8dbb47b400..abb90240871 100644 --- a/homeassistant/components/point/translations/ru.json +++ b/homeassistant/components/point/translations/ru.json @@ -5,7 +5,8 @@ "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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." + "no_flows": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", + "unknown_authorize_url_generation": "\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." }, "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/ca.json b/homeassistant/components/solaredge/translations/ca.json index cce579cd7e5..6705e6d19e8 100644 --- a/homeassistant/components/solaredge/translations/ca.json +++ b/homeassistant/components/solaredge/translations/ca.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "site_exists": "Aquest site_id ja est\u00e0 configurat" }, "error": { - "site_exists": "Aquest site_id ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "could_not_connect": "No s'ha pogut connectar amb l'API de Solaredge", + "invalid_api_key": "Clau API inv\u00e0lida", + "site_exists": "Aquest site_id ja est\u00e0 configurat", + "site_not_active": "El lloc web no est\u00e0 actiu" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/cs.json b/homeassistant/components/solaredge/translations/cs.json index 501ff51a2bb..fab65e36174 100644 --- a/homeassistant/components/solaredge/translations/cs.json +++ b/homeassistant/components/solaredge/translations/cs.json @@ -1,9 +1,13 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven" }, "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "could_not_connect": "Nelze se p\u0159ipojit k API SolarEdge", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven" }, "step": { diff --git a/homeassistant/components/solaredge/translations/es.json b/homeassistant/components/solaredge/translations/es.json index 7a8b55fc649..0447184c309 100644 --- a/homeassistant/components/solaredge/translations/es.json +++ b/homeassistant/components/solaredge/translations/es.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", "site_exists": "Este site_id ya est\u00e1 configurado" }, "error": { - "site_exists": "Este site_id ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "could_not_connect": "No se pudo conectar con la API de solaredge", + "invalid_api_key": "Clave API no v\u00e1lida", + "site_exists": "Este site_id ya est\u00e1 configurado", + "site_not_active": "El sitio no est\u00e1 activo" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/et.json b/homeassistant/components/solaredge/translations/et.json index e3abcedc5c1..4497c536042 100644 --- a/homeassistant/components/solaredge/translations/et.json +++ b/homeassistant/components/solaredge/translations/et.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "site_exists": "See site_id on juba konfigureeritud" }, "error": { - "site_exists": "See site_id on juba konfigureeritud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "could_not_connect": "Ei saanud \u00fchendust Solaredge API-ga", + "invalid_api_key": "Vale API v\u00f5ti", + "site_exists": "See site_id on juba konfigureeritud", + "site_not_active": "Sait pole aktiivne" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index 28b34bdbd3c..0d99250e77a 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" }, "error": { - "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "could_not_connect": "Impossibile connettersi all'API Solaredge", + "invalid_api_key": "Chiave API non valida", + "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato", + "site_not_active": "Il sito non \u00e8 attivo" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/no.json b/homeassistant/components/solaredge/translations/no.json index c6dc9489385..7ff7dd8f144 100644 --- a/homeassistant/components/solaredge/translations/no.json +++ b/homeassistant/components/solaredge/translations/no.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "site_exists": "Denne site_id er allerede konfigurert" }, "error": { - "site_exists": "Denne site_id er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "could_not_connect": "Kunne ikke koble til solaredge API", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "site_exists": "Denne site_id er allerede konfigurert", + "site_not_active": "Nettstedet er ikke aktivt" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/ru.json b/homeassistant/components/solaredge/translations/ru.json index 1dadfbf94ee..fbda2e7eb18 100644 --- a/homeassistant/components/solaredge/translations/ru.json +++ b/homeassistant/components/solaredge/translations/ru.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Solaredge.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "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_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0435\u043d." }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index ab134fff57d..01c1db919cb 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "site_not_active": "\u7db2\u7ad9\u672a\u555f\u7528" }, "step": { "user": { diff --git a/homeassistant/components/srp_energy/translations/it.json b/homeassistant/components/srp_energy/translations/it.json new file mode 100644 index 00000000000..dd8b93a8de2 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_account": "L'ID account deve essere un numero di 9 cifre", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "id": "Account ID", + "is_tou": "E' la tariffa per periodo di utilizzo", + "password": "Password", + "username": "Nome utente" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/ca.json b/homeassistant/components/tellduslive/translations/ca.json index 94c78068dd1..b0fb93241b9 100644 --- a/homeassistant/components/tellduslive/translations/ca.json +++ b/homeassistant/components/tellduslive/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El servei ja est\u00e0 configurat", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" diff --git a/homeassistant/components/tellduslive/translations/en.json b/homeassistant/components/tellduslive/translations/en.json index 80e6cde7acd..7b14df15fa8 100644 --- a/homeassistant/components/tellduslive/translations/en.json +++ b/homeassistant/components/tellduslive/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Service is already configured", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_authorize_url_generation": "Unknown error generating an authorize url." }, "error": { "invalid_auth": "Invalid authentication" diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index 3571ad8e426..81d59e02a12 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -2,9 +2,10 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji", + "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" diff --git a/homeassistant/components/tellduslive/translations/ru.json b/homeassistant/components/tellduslive/translations/ru.json index 0aee4bbd488..0fc0c2f449f 100644 --- a/homeassistant/components/tellduslive/translations/ru.json +++ b/homeassistant/components/tellduslive/translations/ru.json @@ -4,7 +4,8 @@ "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "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\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_authorize_url_generation": "\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." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index 7fc0aef9ec6..b67ff359ceb 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_agreements": "Aquest compte no t\u00e9 pantalles Toon.", - "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json index eda1dcb1ee3..c64913cfb6c 100644 --- a/homeassistant/components/toon/translations/en.json +++ b/homeassistant/components/toon/translations/en.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_agreements": "This account has no Toon displays.", - "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "unknown_authorize_url_generation": "Unknown error generating an authorize url." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/pl.json b/homeassistant/components/toon/translations/pl.json index 2c8715a0044..a41865ddc34 100644 --- a/homeassistant/components/toon/translations/pl.json +++ b/homeassistant/components/toon/translations/pl.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania autoryzowanego adresu URL", + "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon", - "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})" + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json index 4a601de3c28..162608a0f8c 100644 --- a/homeassistant/components/toon/translations/ru.json +++ b/homeassistant/components/toon/translations/ru.json @@ -6,7 +6,8 @@ "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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \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.", "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_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \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 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \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 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "unknown_authorize_url_generation": "\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." }, "step": { "agreement": { From 183154c017c1c303fdf9d87876b18f0bafc140f3 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 25 Nov 2020 08:21:48 +0100 Subject: [PATCH 227/430] Bump pyhs100 dependency to fix hs220 discoverability issues (#43619) Fixes #39395 --- homeassistant/components/tplink/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 14de87b7b69..5b49d8ef1b4 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -4,10 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ - "pyHS100==0.3.5.1" + "pyHS100==0.3.5.2" ], "codeowners": [ "@rytilahti", "@thegardenmonkey" ] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index bd8f81da42f..1b313b36835 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1231,7 +1231,7 @@ pyCEC==0.4.14 pyControl4==0.0.6 # homeassistant.components.tplink -pyHS100==0.3.5.1 +pyHS100==0.3.5.2 # homeassistant.components.met # homeassistant.components.norway_air diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 826229436af..a6a404ebb00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -615,7 +615,7 @@ py17track==2.2.2 pyControl4==0.0.6 # homeassistant.components.tplink -pyHS100==0.3.5.1 +pyHS100==0.3.5.2 # homeassistant.components.met # homeassistant.components.norway_air From c421555db32c2f428bdc04bda0c2cb0f48108d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Wed, 25 Nov 2020 08:25:09 +0100 Subject: [PATCH 228/430] Bump avea to 1.5.1 (#43618) --- homeassistant/components/avea/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 8d39600ed46..bf2b1a6a6ec 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -3,5 +3,5 @@ "name": "Elgato Avea", "documentation": "https://www.home-assistant.io/integrations/avea", "codeowners": ["@pattyland"], - "requirements": ["avea==1.5"] + "requirements": ["avea==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b313b36835..5567a30a054 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -300,7 +300,7 @@ aurorapy==0.2.6 av==8.0.2 # homeassistant.components.avea -# avea==1.5 +# avea==1.5.1 # homeassistant.components.avion # avion==0.10 From 0cf3736162ac8aa5cf52fe2d6b18499875fc0ed9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Nov 2020 08:45:15 +0100 Subject: [PATCH 229/430] Bump ha-ffmpeg to 3.0.2 (#43597) --- homeassistant/components/amcrest/camera.py | 2 +- homeassistant/components/arlo/camera.py | 2 +- homeassistant/components/canary/camera.py | 4 ++-- homeassistant/components/ezviz/camera.py | 2 +- homeassistant/components/ffmpeg/__init__.py | 4 ++-- homeassistant/components/ffmpeg/camera.py | 2 +- homeassistant/components/ffmpeg/manifest.json | 2 +- homeassistant/components/ffmpeg_motion/binary_sensor.py | 4 +--- homeassistant/components/ffmpeg_noise/binary_sensor.py | 4 +--- homeassistant/components/homekit/type_cameras.py | 2 +- homeassistant/components/onvif/camera.py | 4 ++-- homeassistant/components/ring/camera.py | 4 ++-- homeassistant/components/xiaomi/camera.py | 4 ++-- homeassistant/components/yi/camera.py | 5 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ffmpeg/test_sensor.py | 4 ++-- 17 files changed, 24 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 5ac6acb2071..a8aabc233d1 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -236,7 +236,7 @@ class AmcrestCam(Camera): # streaming via ffmpeg streaming_url = self._rtsp_url - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments) try: diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 6f7e3796309..36e32181702 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -88,7 +88,7 @@ class ArloCam(Camera): _LOGGER.error(error_msg) return - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera(video.video_url, extra_cmd=self._ffmpeg_arguments) try: diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index a90eb5a2825..c3fd2a6ff00 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -128,7 +128,7 @@ class CanaryCamera(CoordinatorEntity, Camera): """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) + ffmpeg = ImageFrame(self._ffmpeg.binary) image = await asyncio.shield( ffmpeg.get_image( self._live_stream_session.live_stream_url, @@ -143,7 +143,7 @@ class CanaryCamera(CoordinatorEntity, Camera): if self._live_stream_session is None: return - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera( self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments ) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 701af451496..b9b2463314b 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -217,7 +217,7 @@ class HassEzvizCamera(Camera): async def async_camera_image(self): """Return a frame from the camera stream.""" - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) + ffmpeg = ImageFrame(self._ffmpeg.binary) image = await asyncio.shield( ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index bf44828cdb0..4b67f06ba23 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -97,7 +97,7 @@ async def async_get_image( ): """Get an image from a frame of an RTSP stream.""" manager = hass.data[DATA_FFMPEG] - ffmpeg = ImageFrame(manager.binary, loop=hass.loop) + ffmpeg = ImageFrame(manager.binary) image = await asyncio.shield( ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd) ) @@ -123,7 +123,7 @@ class FFmpegManager: async def async_get_version(self): """Return ffmpeg version.""" - ffversion = FFVersion(self._bin, self.hass.loop) + ffversion = FFVersion(self._bin) self._version = await ffversion.get_version() self._major_version = None diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 6aea28d6509..4cd8b0d1453 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -61,7 +61,7 @@ class FFmpegCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._manager.binary) await stream.open_camera(self._input, extra_cmd=self._extra_arguments) try: diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index aee0b85d056..4e160687d8d 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -2,6 +2,6 @@ "domain": "ffmpeg", "name": "FFmpeg", "documentation": "https://www.home-assistant.io/integrations/ffmpeg", - "requirements": ["ha-ffmpeg==2.0"], + "requirements": ["ha-ffmpeg==3.0.2"], "codeowners": [] } diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 9dbe08ec649..314fbbd2210 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -90,9 +90,7 @@ class FFmpegMotion(FFmpegBinarySensor): """Initialize FFmpeg motion binary sensor.""" super().__init__(config) - self.ffmpeg = ffmpeg_sensor.SensorMotion( - manager.binary, hass.loop, self._async_callback - ) + self.ffmpeg = ffmpeg_sensor.SensorMotion(manager.binary, 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 8425ed173b4..6c84c5973f1 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -53,9 +53,7 @@ class FFmpegNoise(FFmpegBinarySensor): """Initialize FFmpeg noise binary sensor.""" super().__init__(config) - self.ffmpeg = ffmpeg_sensor.SensorNoise( - manager.binary, hass.loop, self._async_callback - ) + self.ffmpeg = ffmpeg_sensor.SensorNoise(manager.binary, self._async_callback) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index fc9d20d61aa..b61a2c57612 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -370,7 +370,7 @@ class Camera(HomeAccessory, PyhapCamera): if self.config[CONF_SUPPORT_AUDIO]: output = output + " " + AUDIO_OUTPUT.format(**output_vars) _LOGGER.debug("FFmpeg output settings: %s", output) - stream = HAFFmpeg(self._ffmpeg.binary, loop=self.driver.loop) + stream = HAFFmpeg(self._ffmpeg.binary) opened = await stream.open( cmd=[], input_source=input_source, output=output, stdout_pipe=False ) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 595924fe40d..50390464df8 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -136,7 +136,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) if image is None: - ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) + ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary) image = await asyncio.shield( ffmpeg.get_image( self._stream_uri, @@ -154,7 +154,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): LOGGER.debug("Handling mjpeg stream from camera '%s'", self.device.name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] - stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop) + stream = CameraMjpeg(ffmpeg_manager.binary) await stream.open_camera( self._stream_uri, diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index a313bcf03ba..bd5950b81a9 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -103,7 +103,7 @@ class RingCam(RingEntityMixin, Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) + ffmpeg = ImageFrame(self._ffmpeg.binary) if self._video_url is None: return @@ -121,7 +121,7 @@ class RingCam(RingEntityMixin, Camera): if self._video_url is None: return - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera(self._video_url) try: diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 97a0aca20f7..359d6c8b896 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -149,7 +149,7 @@ class XiaomiCamera(Camera): 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) + ffmpeg = ImageFrame(self._manager.binary) self._last_image = await asyncio.shield( ffmpeg.get_image( url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments @@ -162,7 +162,7 @@ class XiaomiCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._manager.binary) await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments) try: diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index e669f530197..c130532a2e1 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -123,12 +123,11 @@ class YiCamera(Camera): """Return a still image response from the camera.""" url = await self._get_latest_video_url() if url and url != self._last_url: - ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) + ffmpeg = ImageFrame(self._manager.binary) self._last_image = await asyncio.shield( ffmpeg.get_image( url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments ), - loop=self.hass.loop, ) self._last_url = url @@ -139,7 +138,7 @@ class YiCamera(Camera): if not self._is_on: return - stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) + stream = CameraMjpeg(self._manager.binary) await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments) try: diff --git a/requirements_all.txt b/requirements_all.txt index 5567a30a054..024e15f509a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -720,7 +720,7 @@ gstreamer-player==1.1.2 guppy3==3.1.0 # homeassistant.components.ffmpeg -ha-ffmpeg==2.0 +ha-ffmpeg==3.0.2 # homeassistant.components.philips_js ha-philipsjs==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6a404ebb00..94b279722e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -367,7 +367,7 @@ griddypower==0.1.0 guppy3==3.1.0 # homeassistant.components.ffmpeg -ha-ffmpeg==2.0 +ha-ffmpeg==3.0.2 # homeassistant.components.hangouts hangups==0.4.11 diff --git a/tests/components/ffmpeg/test_sensor.py b/tests/components/ffmpeg/test_sensor.py index 5a89daa624c..a6c9c1f441a 100644 --- a/tests/components/ffmpeg/test_sensor.py +++ b/tests/components/ffmpeg/test_sensor.py @@ -61,7 +61,7 @@ class TestFFmpegNoiseSetup: entity = self.hass.states.get("binary_sensor.ffmpeg_noise") assert entity.state == "off" - self.hass.add_job(mock_ffmpeg.call_args[0][2], True) + self.hass.add_job(mock_ffmpeg.call_args[0][1], True) self.hass.block_till_done() entity = self.hass.states.get("binary_sensor.ffmpeg_noise") @@ -123,7 +123,7 @@ class TestFFmpegMotionSetup: entity = self.hass.states.get("binary_sensor.ffmpeg_motion") assert entity.state == "off" - self.hass.add_job(mock_ffmpeg.call_args[0][2], True) + self.hass.add_job(mock_ffmpeg.call_args[0][1], True) self.hass.block_till_done() entity = self.hass.states.get("binary_sensor.ffmpeg_motion") From b3be708db64849546cba9eba83cc0df63180f58a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Nov 2020 15:10:04 +0100 Subject: [PATCH 230/430] Add default config if not there (#43321) Co-authored-by: Bram Kragten --- .../components/automation/__init__.py | 11 +- .../automation/blueprints/motion_light.yaml | 39 ++++ .../blueprints/notify_leaving_zone.yaml | 34 ++++ homeassistant/components/blueprint/models.py | 33 +++- homeassistant/components/blueprint/schemas.py | 1 + homeassistant/helpers/condition.py | 10 +- homeassistant/helpers/selector.py | 6 + tests/common.py | 4 + .../alarm_control_panel/test_device_action.py | 1 + .../test_device_condition.py | 1 + .../test_device_trigger.py | 1 + .../arcam_fmj/test_device_trigger.py | 1 + tests/components/automation/conftest.py | 3 + tests/components/automation/test_blueprint.py | 185 ++++++++++++++++++ .../binary_sensor/test_device_condition.py | 1 + .../binary_sensor/test_device_trigger.py | 1 + tests/components/blueprint/conftest.py | 14 ++ .../blueprint/test_default_blueprints.py | 28 +++ .../components/climate/test_device_action.py | 1 + .../climate/test_device_condition.py | 1 + .../components/climate/test_device_trigger.py | 1 + tests/components/config/test_automation.py | 1 + .../components/config/test_device_registry.py | 1 + tests/components/cover/test_device_action.py | 1 + .../components/cover/test_device_condition.py | 1 + tests/components/cover/test_device_trigger.py | 1 + .../components/deconz/test_device_trigger.py | 1 + tests/components/default_config/test_init.py | 1 + .../components/device_automation/test_init.py | 1 + .../device_tracker/test_device_condition.py | 1 + .../device_tracker/test_device_trigger.py | 1 + tests/components/fan/test_device_action.py | 1 + tests/components/fan/test_device_condition.py | 1 + tests/components/fan/test_device_trigger.py | 1 + tests/components/geo_location/test_trigger.py | 1 + .../homeassistant/triggers/conftest.py | 3 + .../homekit_controller/test_device_trigger.py | 1 + tests/components/hue/test_device_trigger.py | 1 + .../humidifier/test_device_action.py | 1 + .../humidifier/test_device_condition.py | 1 + .../humidifier/test_device_trigger.py | 1 + tests/components/kodi/test_device_trigger.py | 1 + tests/components/light/test_device_action.py | 1 + .../components/light/test_device_condition.py | 1 + tests/components/light/test_device_trigger.py | 1 + tests/components/litejet/test_trigger.py | 1 + tests/components/lock/test_device_action.py | 1 + .../components/lock/test_device_condition.py | 1 + tests/components/lock/test_device_trigger.py | 1 + .../media_player/test_device_condition.py | 1 + tests/components/mqtt/test_device_trigger.py | 1 + tests/components/mqtt/test_trigger.py | 1 + tests/components/remote/test_device_action.py | 1 + .../remote/test_device_condition.py | 1 + .../components/remote/test_device_trigger.py | 1 + tests/components/search/test_init.py | 1 + .../sensor/test_device_condition.py | 1 + .../components/sensor/test_device_trigger.py | 1 + tests/components/sun/test_trigger.py | 1 + tests/components/switch/test_device_action.py | 1 + .../switch/test_device_condition.py | 1 + .../components/switch/test_device_trigger.py | 1 + tests/components/tag/test_trigger.py | 1 + .../components/tasmota/test_device_trigger.py | 1 + tests/components/template/test_trigger.py | 1 + tests/components/vacuum/test_device_action.py | 1 + .../vacuum/test_device_condition.py | 1 + .../components/vacuum/test_device_trigger.py | 1 + .../water_heater/test_device_action.py | 1 + tests/components/webhook/test_trigger.py | 1 + tests/components/zha/test_device_action.py | 1 + tests/components/zha/test_device_trigger.py | 1 + tests/components/zone/test_trigger.py | 1 + tests/helpers/test_check_config.py | 2 +- tests/helpers/test_selector.py | 20 +- 75 files changed, 432 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/automation/blueprints/motion_light.yaml create mode 100644 homeassistant/components/automation/blueprints/notify_leaving_zone.yaml create mode 100644 tests/components/automation/conftest.py create mode 100644 tests/components/automation/test_blueprint.py create mode 100644 tests/components/blueprint/conftest.py create mode 100644 tests/components/blueprint/test_default_blueprints.py create mode 100644 tests/components/homeassistant/triggers/conftest.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e91aa687b21..e693d2ed814 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -166,7 +166,8 @@ async def async_setup(hass, config): # To register the automation blueprints async_get_blueprints(hass) - await _async_process_config(hass, config, component) + if not await _async_process_config(hass, config, component): + await async_get_blueprints(hass).async_populate() async def trigger_service_handler(entity, service_call): """Handle automation triggers.""" @@ -487,12 +488,13 @@ async def _async_process_config( hass: HomeAssistant, config: Dict[str, Any], component: EntityComponent, -) -> None: +) -> bool: """Process config and add automations. - This method is a coroutine. + Returns if blueprints were used. """ entities = [] + blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): conf: List[Union[Dict[str, Any], blueprint.BlueprintInputs]] = config[ # type: ignore @@ -501,6 +503,7 @@ async def _async_process_config( for list_no, config_block in enumerate(conf): if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore + blueprints_used = True blueprint_inputs = config_block try: @@ -562,6 +565,8 @@ async def _async_process_config( if entities: await component.async_add_entities(entities) + return blueprints_used + async def _async_process_if(hass, config, p_config): """Process if checks.""" diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml new file mode 100644 index 00000000000..aa787e3b2b5 --- /dev/null +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -0,0 +1,39 @@ +blueprint: + name: Motion-activated Light + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml + input: + motion_entity: + name: Motion Sensor + selector: + entity: + domain: binary_sensor + device_class: motion + light_entity: + name: Light + selector: + entity: + domain: light + +# If motion is detected within the 120s delay, +# we restart the script. +mode: restart +max_exceeded: silent + +trigger: + platform: state + entity_id: !placeholder motion_entity + from: "off" + to: "on" + +action: + - service: homeassistant.turn_on + entity_id: !placeholder light_entity + - wait_for_trigger: + platform: state + entity_id: !placeholder motion_entity + from: "on" + to: "off" + - delay: 120 + - service: homeassistant.turn_off + entity_id: !placeholder light_entity diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml new file mode 100644 index 00000000000..7e7dba8bea8 --- /dev/null +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -0,0 +1,34 @@ +blueprint: + name: Send notification when a person leaves a zone + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml + input: + person_entity: + name: Person + selector: + entity: + domain: person + zone_entity: + name: Zone + selector: + entity: + domain: zone + notify_service: + name: The notify service to use + +trigger: + platform: state + entity_id: !placeholder person_entity + +variables: + zone_entity: !placeholder zone_entity + zone_state: "{{ states[zone_entity].name }}" + +condition: + condition: template + value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + +action: + - service: !placeholder notify_service + data: + message: "{{ trigger.to_state.name }} has left {{ zone_state }}" diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 48390d0bd23..e19439133ee 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -2,14 +2,16 @@ import asyncio import logging import pathlib +import shutil from typing import Any, Dict, List, Optional, Union from pkg_resources import parse_version import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant import loader from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, __version__ -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import placeholder from homeassistant.util import yaml @@ -169,6 +171,11 @@ class DomainBlueprints: hass.data.setdefault(DOMAIN, {})[domain] = self + @property + def blueprint_folder(self) -> pathlib.Path: + """Return the blueprint folder.""" + return pathlib.Path(self.hass.config.path(BLUEPRINT_FOLDER, self.domain)) + @callback def async_reset_cache(self) -> None: """Reset the blueprint cache.""" @@ -177,9 +184,7 @@ class DomainBlueprints: def _load_blueprint(self, blueprint_path) -> Blueprint: """Load a blueprint.""" try: - blueprint_data = yaml.load_yaml( - self.hass.config.path(BLUEPRINT_FOLDER, self.domain, blueprint_path) - ) + blueprint_data = yaml.load_yaml(self.blueprint_folder / blueprint_path) except (HomeAssistantError, FileNotFoundError) as err: raise FailedToLoad(self.domain, blueprint_path, err) from err @@ -257,10 +262,7 @@ class DomainBlueprints: async def async_remove_blueprint(self, blueprint_path: str) -> None: """Remove a blueprint file.""" - path = pathlib.Path( - self.hass.config.path(BLUEPRINT_FOLDER, self.domain, blueprint_path) - ) - + path = self.blueprint_folder / blueprint_path await self.hass.async_add_executor_job(path.unlink) self._blueprints[blueprint_path] = None @@ -288,3 +290,18 @@ class DomainBlueprints: ) self._blueprints[blueprint_path] = blueprint + + async def async_populate(self) -> None: + """Create folder if it doesn't exist and populate with examples.""" + integration = await loader.async_get_integration(self.hass, self.domain) + + def populate(): + if self.blueprint_folder.exists(): + return + + shutil.copytree( + integration.file_path / BLUEPRINT_FOLDER, + self.blueprint_folder / HA_DOMAIN, + ) + + await self.hass.async_add_executor_job(populate) diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index ed78b4d2b42..900497c3f71 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -63,6 +63,7 @@ BLUEPRINT_SCHEMA = vol.Schema( vol.Required(CONF_BLUEPRINT): vol.Schema( { vol.Required(CONF_NAME): str, + vol.Optional(CONF_DESCRIPTION): str, vol.Required(CONF_DOMAIN): str, vol.Optional(CONF_SOURCE_URL): cv.url, vol.Optional(CONF_HOMEASSISTANT): { diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 9cfa6c00996..5ace4c91bcf 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -453,18 +453,12 @@ def async_template( ) -> bool: """Test if template condition matches.""" try: - value = value_template.async_render(variables) + value: str = value_template.async_render(variables, parse_result=False) except TemplateError as ex: _LOGGER.error("Error during template condition: %s", ex) return False - if isinstance(value, bool): - return value - - if isinstance(value, str): - return value.lower() == "true" - - return False + return value.lower() == "true" def async_template_from_config( diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 5bf70f01bfa..d3e96a5205e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -23,6 +23,10 @@ def validate_selector(config: Any) -> Dict: if selector_class is None: raise vol.Invalid(f"Unknown selector type {selector_type} found") + # Seletors can be empty + if config[selector_type] is None: + return {selector_type: {}} + return { selector_type: cast(Dict, selector_class.CONFIG_SCHEMA(config[selector_type])) } @@ -44,6 +48,8 @@ class EntitySelector(Selector): vol.Optional("integration"): str, # Domain the entity belongs to vol.Optional("domain"): str, + # Device class of the entity + vol.Optional("device_class"): str, } ) diff --git a/tests/common.py b/tests/common.py index f0994308fe6..dbe2cbfd42a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -9,6 +9,7 @@ from io import StringIO import json import logging import os +import pathlib import threading import time import uuid @@ -704,6 +705,9 @@ def patch_yaml_files(files_dict, endswith=True): def mock_open_f(fname, **_): """Mock open() in the yaml module, used by load_yaml.""" # Return the mocked file on full match + if isinstance(fname, pathlib.Path): + fname = str(fname) + if fname in files_dict: _LOGGER.debug("patch_yaml_files match %s", fname) res = StringIO(files_dict[fname]) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 74c98da3189..514e8fa81f2 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -23,6 +23,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index fcb2ba5a09b..33f717e1893 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -22,6 +22,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 9d414d179ab..82432bc37ab 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -22,6 +22,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index 2a119fd2017..0f2cfaf2893 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -13,6 +13,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/automation/conftest.py b/tests/components/automation/conftest.py new file mode 100644 index 00000000000..a967e0af192 --- /dev/null +++ b/tests/components/automation/conftest.py @@ -0,0 +1,3 @@ +"""Conftest for automation tests.""" + +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py new file mode 100644 index 00000000000..a601998a00d --- /dev/null +++ b/tests/components/automation/test_blueprint.py @@ -0,0 +1,185 @@ +"""Test built-in blueprints.""" +import asyncio +import contextlib +from datetime import timedelta +import pathlib + +from homeassistant.components import automation +from homeassistant.components.blueprint import models +from homeassistant.core import callback +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util, yaml + +from tests.async_mock import patch +from tests.common import async_fire_time_changed, async_mock_service + +BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(automation.__file__).parent / "blueprints" + + +@contextlib.contextmanager +def patch_blueprint(blueprint_path: str, data_path): + """Patch blueprint loading from a different source.""" + orig_load = models.DomainBlueprints._load_blueprint + + @callback + def mock_load_blueprint(self, path): + if path != blueprint_path: + assert False, f"Unexpected blueprint {path}" + return orig_load(self, path) + + return models.Blueprint( + yaml.load_yaml(data_path), expected_domain=self.domain, path=path + ) + + with patch( + "homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint", + mock_load_blueprint, + ): + yield + + +async def test_notify_leaving_zone(hass): + """Test notifying leaving a zone blueprint.""" + + def set_person_state(state, extra={}): + hass.states.async_set( + "person.test_person", state, {"friendly_name": "Paulus", **extra} + ) + + set_person_state("School") + + assert await async_setup_component( + hass, "zone", {"zone": {"name": "School", "latitude": 1, "longitude": 2}} + ) + + with patch_blueprint( + "notify_leaving_zone.yaml", + BUILTIN_BLUEPRINT_FOLDER / "notify_leaving_zone.yaml", + ): + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "use_blueprint": { + "path": "notify_leaving_zone.yaml", + "input": { + "person_entity": "person.test_person", + "zone_entity": "zone.school", + "notify_service": "notify.test_service", + }, + } + } + }, + ) + + calls = async_mock_service(hass, "notify", "test_service") + + # Leaving zone to no zone + set_person_state("not_home") + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["message"] == "Paulus has left School" + + # Should not increase when we go to another zone + set_person_state("bla") + await hass.async_block_till_done() + + assert len(calls) == 1 + + # Should not increase when we go into the zone + set_person_state("School") + await hass.async_block_till_done() + + assert len(calls) == 1 + + # Should not increase when we move in the zone + set_person_state("School", {"extra_key": "triggers change with same state"}) + await hass.async_block_till_done() + + assert len(calls) == 1 + + # Should increase when leaving zone for another zone + set_person_state("Just Outside School") + await hass.async_block_till_done() + + assert len(calls) == 2 + + +async def test_motion_light(hass): + """Test motion light blueprint.""" + hass.states.async_set("binary_sensor.kitchen", "off") + + with patch_blueprint( + "motion_light.yaml", + BUILTIN_BLUEPRINT_FOLDER / "motion_light.yaml", + ): + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "use_blueprint": { + "path": "motion_light.yaml", + "input": { + "light_entity": "light.kitchen", + "motion_entity": "binary_sensor.kitchen", + }, + } + } + }, + ) + + turn_on_calls = async_mock_service(hass, "homeassistant", "turn_on") + turn_off_calls = async_mock_service(hass, "homeassistant", "turn_off") + + # Turn on motion + hass.states.async_set("binary_sensor.kitchen", "on") + # Can't block till done because delay is active + # So wait 5 event loop iterations to process script + for _ in range(5): + await asyncio.sleep(0) + + assert len(turn_on_calls) == 1 + + # Test light doesn't turn off if motion stays + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + + for _ in range(5): + await asyncio.sleep(0) + + assert len(turn_off_calls) == 0 + + # Test light turns off off 120s after last motion + hass.states.async_set("binary_sensor.kitchen", "off") + + for _ in range(5): + await asyncio.sleep(0) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120)) + await hass.async_block_till_done() + + assert len(turn_off_calls) == 1 + + # Test restarting the script + hass.states.async_set("binary_sensor.kitchen", "on") + + for _ in range(5): + await asyncio.sleep(0) + + assert len(turn_on_calls) == 2 + assert len(turn_off_calls) == 1 + + hass.states.async_set("binary_sensor.kitchen", "off") + + for _ in range(5): + await asyncio.sleep(0) + + hass.states.async_set("binary_sensor.kitchen", "on") + + for _ in range(15): + await asyncio.sleep(0) + + assert len(turn_on_calls) == 3 + assert len(turn_off_calls) == 1 diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 9fd50277d23..7a7e80e36a3 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -20,6 +20,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index cea0103acdb..fef109eb9d5 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -20,6 +20,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/blueprint/conftest.py b/tests/components/blueprint/conftest.py new file mode 100644 index 00000000000..c8110ddaf08 --- /dev/null +++ b/tests/components/blueprint/conftest.py @@ -0,0 +1,14 @@ +"""Blueprints conftest.""" + +import pytest + +from tests.async_mock import patch + + +@pytest.fixture(autouse=True) +def stub_blueprint_populate(): + """Stub copying the blueprint automations to the config folder.""" + with patch( + "homeassistant.components.blueprint.models.DomainBlueprints.async_populate" + ): + yield diff --git a/tests/components/blueprint/test_default_blueprints.py b/tests/components/blueprint/test_default_blueprints.py new file mode 100644 index 00000000000..5e4016fcb88 --- /dev/null +++ b/tests/components/blueprint/test_default_blueprints.py @@ -0,0 +1,28 @@ +"""Test default blueprints.""" +import importlib +import logging +import pathlib + +import pytest + +from homeassistant.components.blueprint import models +from homeassistant.components.blueprint.const import BLUEPRINT_FOLDER +from homeassistant.util import yaml + +DOMAINS = ["automation"] +LOGGER = logging.getLogger(__name__) + + +@pytest.mark.parametrize("domain", DOMAINS) +def test_default_blueprints(domain: str): + """Validate a folder of blueprints.""" + integration = importlib.import_module(f"homeassistant.components.{domain}") + blueprint_folder = pathlib.Path(integration.__file__).parent / BLUEPRINT_FOLDER + items = list(blueprint_folder.glob("*")) + assert len(items) > 0, "Folder cannot be empty" + + for fil in items: + LOGGER.info("Processing %s", fil) + assert fil.name.endswith(".yaml") + data = yaml.load_yaml(fil) + models.Blueprint(data, expected_domain=domain) diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index ff78b837591..4084d37358e 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 431849ae761..8e6d5829c41 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 58aa3311771..69bb4626e49 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 1d160870169..347ac96f892 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -5,6 +5,7 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from tests.async_mock import patch +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa async def test_get_device_config(hass, hass_client): diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 1f82434c7a6..1e1cbccf60a 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.config import device_registry from tests.common import mock_device_registry +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index d302353582c..cad6074ff34 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 511c7ced898..b3098ceeca9 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -22,6 +22,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 1996cf9d6df..ab054ad8223 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -22,6 +22,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 69c9ae94727..a5399fe4796 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration from tests.common import assert_lists_same, async_get_device_automations +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa SENSORS = { "1": { diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 6bf9cc44c56..a4a5898982b 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -4,6 +4,7 @@ import pytest from homeassistant.setup import async_setup_component from tests.async_mock import patch +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture(autouse=True) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 19786bb08e8..83ec146b53e 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -13,6 +13,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 950ace24335..a187f21e954 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 963dae3127d..2f0ec14ec4b 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa AWAY_LATITUDE = 32.881011 AWAY_LONGITUDE = -117.234758 diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 70da4bd1fca..d3a9aedcf9c 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -14,6 +14,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index 939fee154c5..6725587aeda 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index c46b3a6fcec..d96f0a828f3 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index 8bf1c6abe15..ab984a2c309 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -7,6 +7,7 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_component +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/homeassistant/triggers/conftest.py b/tests/components/homeassistant/triggers/conftest.py new file mode 100644 index 00000000000..5c983ba698e --- /dev/null +++ b/tests/components/homeassistant/triggers/conftest.py @@ -0,0 +1,3 @@ +"""Conftest for HA triggers.""" + +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index c8ef2cbef38..9de9a30f99f 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -12,6 +12,7 @@ from tests.common import ( async_get_device_automations, async_mock_service, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa from tests.components.homekit_controller.common import setup_test_component diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py index b6d3f4f2f50..0975c644e61 100644 --- a/tests/components/hue/test_device_trigger.py +++ b/tests/components/hue/test_device_trigger.py @@ -15,6 +15,7 @@ from tests.common import ( async_mock_service, mock_device_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa REMOTES_RESPONSE = {"7": HUE_TAP_REMOTE_1, "8": HUE_DIMMER_REMOTE_1} diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index 91b7819e18b..93b97408c39 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index 76a850887ca..ad001d52ae0 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -17,6 +17,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 4f93b4be4de..7cd736b79f4 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -20,6 +20,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index de2d69212ef..8cf6c635393 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index aeedde82af5..63670d9bfab 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -21,6 +21,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 2a43a0abebe..eea443b7d34 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index d3c630cd0dc..fad39898467 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index b8699785d5e..3cbbd474b88 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -11,6 +11,7 @@ import homeassistant.components.automation as automation import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, async_mock_service +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index dbf390df57b..91cab9cdaf4 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index c2db984f16f..949100daa55 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 006df742c6d..20674c483fd 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -15,6 +15,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index c52daa80320..c7668d748af 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -21,6 +21,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 844f34e8d4f..a46406c330f 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index b35082ef2c6..a26ecadb6bd 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -7,6 +7,7 @@ from homeassistant.setup import async_setup_component from tests.async_mock import ANY from tests.common import async_fire_mqtt_message, async_mock_service, mock_component +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 89d0f0de6bf..17165639e25 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 2f01b4ab55f..e5a9bc3a9c9 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 73feb3ea08f..eccf96c04f6 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index a379b91f82a..5710fa04698 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -3,6 +3,7 @@ from homeassistant.components import search from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa async def test_search(hass): diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 0c0c9c6c22b..9a023d6f5ad 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa 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 dda57de0d9d..c39b4597632 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -20,6 +20,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 2cb21051200..1a3de56964c 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -18,6 +18,7 @@ import homeassistant.util.dt as dt_util from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service, mock_component +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 204b2370cb8..4da401a215c 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -16,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 6e542ee24d1..093758cbe62 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 512942ca71b..34817b687f8 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 0249acaf29b..3a83c8e5d2b 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -8,6 +8,7 @@ from homeassistant.components.tag.const import DOMAIN, TAG_ID from homeassistant.setup import async_setup_component from tests.common import async_mock_service +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 09c3d691b09..42fc5dc7a49 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -18,6 +18,7 @@ from tests.common import ( async_fire_mqtt_message, async_get_device_automations, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 4bda4dc23ca..828cf1fb7b4 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -17,6 +17,7 @@ from tests.common import ( async_mock_service, mock_component, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index 47ce5423f7d..3edeaba2a41 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -14,6 +14,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 16715266b8c..3dc7a628741 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -19,6 +19,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index f3439700e33..e3f615891e6 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -14,6 +14,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index 3d3e70444b7..06bd43ec654 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -14,6 +14,7 @@ from tests.common import ( mock_device_registry, mock_registry, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index bb3cb680743..46459bd88c4 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -5,6 +5,7 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from tests.async_mock import patch +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture(autouse=True) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index de3a4eb1296..c0350ce63a5 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -16,6 +16,7 @@ from homeassistant.helpers.device_registry import async_get_registry from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_coro +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa SHORT_PRESS = "remote_button_short_press" COMMAND = "command" diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 801b6831379..b72f693e531 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -19,6 +19,7 @@ from tests.common import ( async_get_device_automations, async_mock_service, ) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa ON = 1 OFF = 0 diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index 0477f9bead7..d7f5857b466 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -7,6 +7,7 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_component +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @pytest.fixture diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 786fa986a14..1c25dbedcde 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -165,4 +165,4 @@ action: } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) - assert len(res["automation"]) == 1 + assert len(res.get("automation", [])) == 1 diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 51490567d8e..a8410821c42 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -6,7 +6,25 @@ from homeassistant.helpers import selector @pytest.mark.parametrize( - "schema", ({}, {"non_existing": {}}, {"device": {}, "entity": {}}) + "schema", + ( + {"device": None}, + {"entity": None}, + ), +) +def test_valid_base_schema(schema): + """Test base schema validation.""" + selector.validate_selector(schema) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"non_existing": {}}, + # Two keys + {"device": {}, "entity": {}}, + ), ) def test_invalid_base_schema(schema): """Test base schema validation.""" From ebaf143cf6f850765c63b0af813b24f110fa49ec Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 25 Nov 2020 16:29:01 +0200 Subject: [PATCH 231/430] Support for Shelly Input Events (#43479) --- homeassistant/components/shelly/__init__.py | 54 ++++++++++++++++++--- homeassistant/components/shelly/const.py | 10 ++++ homeassistant/components/shelly/entity.py | 6 +-- homeassistant/components/shelly/utils.py | 38 ++++++++------- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 14daffb4221..3c34da574a6 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -27,6 +27,7 @@ from .const import ( COAP, DATA_CONFIG_ENTRY, DOMAIN, + INPUTS_EVENTS_DICT, POLLING_TIMEOUT_MULTIPLIER, REST, REST_SENSORS_UPDATE_INTERVAL, @@ -34,6 +35,7 @@ from .const import ( SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) +from .utils import get_device_name PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) @@ -54,11 +56,6 @@ async def get_coap_context(hass): return context -def get_device_name(device): - """Naming for device.""" - return device.settings["name"] or device.settings["device"]["hostname"] - - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Shelly component.""" hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} @@ -113,6 +110,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): def __init__(self, hass, entry, device: aioshelly.Device): """Initialize the Shelly device wrapper.""" + self.device_id = None sleep_mode = device.settings.get("sleep_mode") if sleep_mode: @@ -140,6 +138,46 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device.subscribe_updates(self.async_set_updated_data) + self._async_remove_input_events_handler = self.async_add_listener( + self._async_input_events_handler + ) + self._last_input_events_count = dict() + + @callback + def _async_input_events_handler(self): + """Handle device input events.""" + for block in self.device.blocks: + if ( + "inputEvent" not in block.sensor_ids + or "inputEventCnt" not in block.sensor_ids + ): + continue + + channel = int(block.channel or 0) + 1 + event_type = block.inputEvent + last_event_count = self._last_input_events_count.get(channel) + self._last_input_events_count[channel] = block.inputEventCnt + + if last_event_count == block.inputEventCnt or event_type == "": + continue + + if event_type in INPUTS_EVENTS_DICT: + self.hass.bus.async_fire( + "shelly.click", + { + "device_id": self.device_id, + "device": self.device.settings["device"]["hostname"], + "channel": channel, + "click_type": INPUTS_EVENTS_DICT[event_type], + }, + ) + else: + _LOGGER.warning( + "Shelly input event %s for device %s is not supported, please open issue", + event_type, + self.name, + ) + async def _async_update_data(self): """Fetch data.""" _LOGGER.debug("Polling Shelly Device - %s", self.name) @@ -166,7 +204,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Set up the wrapper.""" dev_reg = await device_registry.async_get_registry(self.hass) model_type = self.device.settings["device"]["type"] - dev_reg.async_get_or_create( + entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, name=self.name, connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, @@ -176,10 +214,12 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): model=aioshelly.MODEL_NAMES.get(model_type, model_type), sw_version=self.device.settings["fw"], ) + self.device_id = entry.id def shutdown(self): """Shutdown the wrapper.""" self.device.shutdown() + self._async_remove_input_events_handler() class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): @@ -200,7 +240,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Fetch data.""" try: async with async_timeout.timeout(5): - _LOGGER.debug("REST update for %s", get_device_name(self.device)) + _LOGGER.debug("REST update for %s", self.name) return await self.device.update_status() except OSError as err: raise update_coordinator.UpdateFailed("Error fetching data") from err diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 36ab095f616..cd747466973 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -22,3 +22,13 @@ UPDATE_PERIOD_MULTIPLIER = 2.2 # Shelly Air - Maximum work hours before lamp replacement SHAIR_MAX_WORK_HOURS = 9000 + +# Map Shelly input events +INPUTS_EVENTS_DICT = { + "S": "single", + "SS": "double", + "SSS": "triple", + "L": "long", + "SL": "single_long", + "LS": "long_single", +} diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 9b834db923c..b99f32be783 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -116,7 +116,7 @@ class ShellyBlockEntity(entity.Entity): """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block - self._name = get_entity_name(wrapper, block) + self._name = get_entity_name(wrapper.device, block) @property def name(self): @@ -182,7 +182,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit = unit self._unique_id = f"{super().unique_id}-{self.attribute}" - self._name = get_entity_name(wrapper, block, self.description.name) + self._name = get_entity_name(wrapper.device, block, self.description.name) @property def unique_id(self): @@ -255,7 +255,7 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): self.description = description self._unit = self.description.unit - self._name = get_entity_name(wrapper, None, self.description.name) + self._name = get_entity_name(wrapper.device, None, self.description.name) self.path = self.description.path self._attributes = self.description.attributes diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index e6981db2a0d..7c72f262716 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -9,7 +9,6 @@ import aioshelly from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from . import ShellyDeviceWrapper from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -31,22 +30,31 @@ def temperature_unit(block_info: dict) -> str: return TEMP_CELSIUS +def get_device_name(device: aioshelly.Device) -> str: + """Naming for device.""" + return device.settings["name"] or device.settings["device"]["hostname"] + + def get_entity_name( - wrapper: ShellyDeviceWrapper, + device: aioshelly.Device, block: aioshelly.Block, description: Optional[str] = None, -): +) -> str: """Naming for switch and sensors.""" - entity_name = wrapper.name + entity_name = get_device_name(device) if block: channels = None if block.type == "input": - channels = wrapper.device.shelly.get("num_inputs") + # Shelly Dimmer/1L has two input channels and missing "num_inputs" + if device.settings["device"]["type"] in ["SHDM-1", "SHDM-2", "SHSW-L"]: + channels = 2 + else: + channels = device.shelly.get("num_inputs") elif block.type == "emeter": - channels = wrapper.device.shelly.get("num_emeters") + channels = device.shelly.get("num_emeters") elif block.type in ["relay", "light"]: - channels = wrapper.device.shelly.get("num_outputs") + channels = device.shelly.get("num_outputs") elif block.type in ["roller", "device"]: channels = 1 @@ -55,21 +63,17 @@ def get_entity_name( if channels > 1 and block.type != "device": entity_name = None mode = block.type + "s" - if mode in wrapper.device.settings: - entity_name = wrapper.device.settings[mode][int(block.channel)].get( - "name" - ) + if mode in device.settings: + entity_name = device.settings[mode][int(block.channel)].get("name") if not entity_name: - if wrapper.model == "SHEM-3": + if device.settings["device"]["type"] == "SHEM-3": base = ord("A") else: base = ord("1") - entity_name = f"{wrapper.name} channel {chr(int(block.channel)+base)}" - - # Shelly Dimmer has two input channels and missing "num_inputs" - if wrapper.model in ["SHDM-1", "SHDM-2"] and block.type == "input": - entity_name = f"{entity_name} channel {int(block.channel)+1}" + entity_name = ( + f"{get_device_name(device)} channel {chr(int(block.channel)+base)}" + ) if description: entity_name = f"{entity_name} {description}" From b7f36106dffb05a4ec547917077679b82f9888be Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 25 Nov 2020 15:37:07 +0100 Subject: [PATCH 232/430] Add Shelly UNI ADC sensor (#43490) --- homeassistant/components/shelly/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 10d15fdd62f..f4dfd16aa25 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -158,6 +158,12 @@ SENSORS = { "Operational hours": round(block.totalWorkTime / 3600, 1) }, ), + ("adc", "adc"): BlockAttributeDescription( + name="ADC", + unit=VOLT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_VOLTAGE, + ), } REST_SENSORS = { From 1de2554f706d424159d2efa6a734fbfea82ffc22 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Nov 2020 16:10:33 +0100 Subject: [PATCH 233/430] Tweak template digit detection (#43621) --- homeassistant/helpers/template.py | 2 +- tests/helpers/test_template.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 09a9170c79a..fb3e6ba40b5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -50,7 +50,7 @@ _ENVIRONMENT = "template.environment" _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#") # Match "simple" ints and floats. -1.0, 1, +5, 5.0 -_IS_NUMERIC = re.compile(r"^[+-]?\d*(?:\.\d*)?$") +_IS_NUMERIC = re.compile(r"^[+-]?(?!0\d)\d*(?:\.\d*)?$") _RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunction"} diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 53186ed35a1..c8a8bc0710c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2421,11 +2421,17 @@ async def test_parse_result(hass): ("1e+100", "1e+100"), ("0xface", "0xface"), ("123", 123), + ("10", 10), ("123.0", 123.0), (".5", 0.5), + ("0.5", 0.5), ("-1", -1), ("-1.0", -1.0), ("+1", 1), ("5.", 5.0), + ("123_123_123", "123_123_123"), + # ("+48100200300", "+48100200300"), # phone number + ("010", "010"), + ("0011101.00100001010001", "0011101.00100001010001"), ): assert template.Template(tpl, hass).async_render() == result From ea52ffc2d97a823d90a92c8f543aa790c25ab18f Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 25 Nov 2020 16:38:49 +0100 Subject: [PATCH 234/430] Add FireServiceRota/BrandweerRooster integration (#38206) Co-authored-by: Robert Svensson Co-authored-by: Paulus Schoutsen --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/fireservicerota/__init__.py | 246 ++++++++++++++++++ .../components/fireservicerota/config_flow.py | 129 +++++++++ .../components/fireservicerota/const.py | 9 + .../components/fireservicerota/manifest.json | 8 + .../components/fireservicerota/sensor.py | 128 +++++++++ .../components/fireservicerota/strings.json | 29 +++ .../fireservicerota/translations/en.json | 30 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/fireservicerota/__init__.py | 1 + .../fireservicerota/test_config_flow.py | 114 ++++++++ 14 files changed, 705 insertions(+) create mode 100644 homeassistant/components/fireservicerota/__init__.py create mode 100644 homeassistant/components/fireservicerota/config_flow.py create mode 100644 homeassistant/components/fireservicerota/const.py create mode 100644 homeassistant/components/fireservicerota/manifest.json create mode 100644 homeassistant/components/fireservicerota/sensor.py create mode 100644 homeassistant/components/fireservicerota/strings.json create mode 100644 homeassistant/components/fireservicerota/translations/en.json create mode 100644 tests/components/fireservicerota/__init__.py create mode 100644 tests/components/fireservicerota/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index e256be60466..bf6c81bf514 100644 --- a/.coveragerc +++ b/.coveragerc @@ -262,6 +262,9 @@ omit = homeassistant/components/fibaro/* homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py + homeassistant/components/fireservicerota/__init__.py + homeassistant/components/fireservicerota/const.py + homeassistant/components/fireservicerota/sensor.py homeassistant/components/firmata/__init__.py homeassistant/components/firmata/binary_sensor.py homeassistant/components/firmata/board.py diff --git a/CODEOWNERS b/CODEOWNERS index 1c63bd37c45..23eacec8f22 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ homeassistant/components/ezviz/* @baqs homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes +homeassistant/components/fireservicerota/* @cyberjunky homeassistant/components/firmata/* @DaAwesomeP homeassistant/components/fixer/* @fabaff homeassistant/components/flick_electric/* @ZephireNZ diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py new file mode 100644 index 00000000000..a0fc1d68d23 --- /dev/null +++ b/homeassistant/components/fireservicerota/__init__.py @@ -0,0 +1,246 @@ +"""The FireServiceRota integration.""" +import asyncio +from datetime import timedelta +import logging + +from pyfireservicerota import ( + ExpiredTokenError, + FireServiceRota, + FireServiceRotaIncidents, + InvalidAuthError, + InvalidTokenError, +) + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, WSS_BWRURL + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_PLATFORMS = {SENSOR_DOMAIN} + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the FireServiceRota component.""" + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up FireServiceRota from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + coordinator = FireServiceRotaCoordinator(hass, entry) + await coordinator.setup() + await coordinator.async_availability_update() + + if coordinator.token_refresh_failure: + return False + + hass.data[DOMAIN][entry.entry_id] = coordinator + + for platform in SUPPORTED_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload FireServiceRota config entry.""" + + hass.data[DOMAIN][entry.entry_id].websocket.stop_listener() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in SUPPORTED_PLATFORMS + ] + ) + ) + + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + + return unload_ok + + +class FireServiceRotaOauth: + """Handle authentication tokens.""" + + def __init__(self, hass, entry, fsr): + """Initialize the oauth object.""" + self._hass = hass + self._entry = entry + + self._url = entry.data[CONF_URL] + self._username = entry.data[CONF_USERNAME] + self._fsr = fsr + + async def async_refresh_tokens(self) -> bool: + """Refresh tokens and update config entry.""" + _LOGGER.debug("Refreshing authentication tokens after expiration") + + try: + token_info = await self._hass.async_add_executor_job( + self._fsr.refresh_tokens + ) + + except (InvalidAuthError, InvalidTokenError): + _LOGGER.error("Error refreshing tokens, triggered reauth workflow") + self._hass.add_job( + self._hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + **self._entry.data, + }, + ) + ) + + return False + + _LOGGER.debug("Saving new tokens in config entry") + self._hass.config_entries.async_update_entry( + self._entry, + data={ + "auth_implementation": DOMAIN, + CONF_URL: self._url, + CONF_USERNAME: self._username, + CONF_TOKEN: token_info, + }, + ) + + return True + + +class FireServiceRotaWebSocket: + """Define a FireServiceRota websocket manager object.""" + + def __init__(self, hass, entry): + """Initialize the websocket object.""" + self._hass = hass + self._entry = entry + + self._fsr_incidents = FireServiceRotaIncidents(on_incident=self._on_incident) + self._incident_data = None + + def _construct_url(self) -> str: + """Return URL with latest access token.""" + return WSS_BWRURL.format( + self._entry.data[CONF_URL], self._entry.data[CONF_TOKEN]["access_token"] + ) + + def incident_data(self) -> object: + """Return incident data.""" + return self._incident_data + + def _on_incident(self, data) -> None: + """Received new incident, update data.""" + _LOGGER.debug("Received new incident via websocket: %s", data) + self._incident_data = data + dispatcher_send(self._hass, f"{DOMAIN}_{self._entry.entry_id}_update") + + def start_listener(self) -> None: + """Start the websocket listener.""" + _LOGGER.debug("Starting incidents listener") + self._fsr_incidents.start(self._construct_url()) + + def stop_listener(self) -> None: + """Stop the websocket listener.""" + _LOGGER.debug("Stopping incidents listener") + self._fsr_incidents.stop() + + +class FireServiceRotaCoordinator(DataUpdateCoordinator): + """Getting the latest data from fireservicerota.""" + + def __init__(self, hass, entry): + """Initialize the data object.""" + self._hass = hass + self._entry = entry + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_method=self.async_availability_update, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + + self._url = entry.data[CONF_URL] + self._tokens = entry.data[CONF_TOKEN] + + self.token_refresh_failure = False + self.incident_id = None + + self.fsr = FireServiceRota(base_url=self._url, token_info=self._tokens) + + self.oauth = FireServiceRotaOauth( + self._hass, + self._entry, + self.fsr, + ) + + self.websocket = FireServiceRotaWebSocket(self._hass, self._entry) + + async def setup(self) -> None: + """Set up the coordinator.""" + await self._hass.async_add_executor_job(self.websocket.start_listener) + + async def update_call(self, func, *args): + """Perform update call and return data.""" + if self.token_refresh_failure: + return + + try: + return await self._hass.async_add_executor_job(func, *args) + except (ExpiredTokenError, InvalidTokenError): + self.websocket.stop_listener() + self.token_refresh_failure = True + self.update_interval = None + + if await self.oauth.async_refresh_tokens(): + self.update_interval = MIN_TIME_BETWEEN_UPDATES + self.token_refresh_failure = False + self.websocket.start_listener() + + return await self._hass.async_add_executor_job(func, *args) + + async def async_availability_update(self) -> None: + """Get the latest availability data.""" + _LOGGER.debug("Updating availability data") + + return await self.update_call( + self.fsr.get_availability, str(self._hass.config.time_zone) + ) + + async def async_response_update(self) -> object: + """Get the latest incident response data.""" + data = self.websocket.incident_data() + if data is None or "id" not in data: + return + + self.incident_id = data("id") + _LOGGER.debug("Updating incident response data for id: %s", self.incident_id) + + return await self.update_call(self.fsr.get_incident_response, self.incident_id) + + async def async_set_response(self, value) -> None: + """Set incident response status.""" + _LOGGER.debug( + "Setting incident response for incident '%s' to status '%s'", + self.incident_id, + value, + ) + + await self.update_call(self.fsr.set_incident_response, self.incident_id, value) diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py new file mode 100644 index 00000000000..e5c49fda6ad --- /dev/null +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -0,0 +1,129 @@ +"""Config flow for FireServiceRota.""" +from pyfireservicerota import FireServiceRota, InvalidAuthError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME + +from .const import DOMAIN, URL_LIST # pylint: disable=unused-import + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL, default="www.brandweerrooster.nl"): vol.In(URL_LIST), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a FireServiceRota config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize config flow.""" + self.api = None + self._base_url = None + self._username = None + self._password = None + self._existing_entry = None + self._description_placeholders = None + + 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, errors) + + return await self._validate_and_create_entry(user_input, "user") + + async def _validate_and_create_entry(self, user_input, step_id): + """Check if config is valid and create entry if so.""" + self._password = user_input[CONF_PASSWORD] + + extra_inputs = user_input + + if self._existing_entry: + extra_inputs = self._existing_entry + + self._username = extra_inputs[CONF_USERNAME] + self._base_url = extra_inputs[CONF_URL] + + if self.unique_id is None: + await self.async_set_unique_id(self._username) + self._abort_if_unique_id_configured() + + try: + self.api = FireServiceRota( + base_url=self._base_url, + username=self._username, + password=self._password, + ) + token_info = await self.hass.async_add_executor_job(self.api.request_tokens) + + except InvalidAuthError: + self.api = None + return self.async_show_form( + step_id=step_id, + data_schema=DATA_SCHEMA, + errors={"base": "invalid_auth"}, + ) + + data = { + "auth_implementation": DOMAIN, + CONF_URL: self._base_url, + CONF_USERNAME: self._username, + CONF_TOKEN: token_info, + } + + if step_id == "user": + return self.async_create_entry(title=self._username, data=data) + + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + def _show_setup_form(self, user_input=None, errors=None, step_id="user"): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + if step_id == "user": + schema = { + vol.Required(CONF_URL, default="www.brandweerrooster.nl"): vol.In( + URL_LIST + ), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + else: + schema = {vol.Required(CONF_PASSWORD): str} + + return self.async_show_form( + step_id=step_id, + data_schema=vol.Schema(schema), + errors=errors or {}, + description_placeholders=self._description_placeholders, + ) + + async def async_step_reauth(self, user_input=None): + """Get new tokens for a config entry that can't authenticate.""" + + if not self._existing_entry: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._existing_entry = user_input.copy() + self._description_placeholders = {"username": user_input[CONF_USERNAME]} + user_input = None + + if user_input is None: + return self._show_setup_form(step_id=config_entries.SOURCE_REAUTH) + + return await self._validate_and_create_entry( + user_input, config_entries.SOURCE_REAUTH + ) diff --git a/homeassistant/components/fireservicerota/const.py b/homeassistant/components/fireservicerota/const.py new file mode 100644 index 00000000000..5ca0b7d7e64 --- /dev/null +++ b/homeassistant/components/fireservicerota/const.py @@ -0,0 +1,9 @@ +"""Constants for the FireServiceRota integration.""" + +DOMAIN = "fireservicerota" + +URL_LIST = { + "www.brandweerrooster.nl": "BrandweerRooster", + "www.fireservicerota.co.uk": "FireServiceRota", +} +WSS_BWRURL = "wss://{0}/cable?access_token={1}" diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json new file mode 100644 index 00000000000..6485d155f50 --- /dev/null +++ b/homeassistant/components/fireservicerota/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "fireservicerota", + "name": "FireServiceRota", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/fireservicerota", + "requirements": ["pyfireservicerota==0.0.40"], + "codeowners": ["@cyberjunky"] +} diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py new file mode 100644 index 00000000000..4360a834288 --- /dev/null +++ b/homeassistant/components/fireservicerota/sensor.py @@ -0,0 +1,128 @@ +"""Sensor platform for FireServiceRota integration.""" +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN as FIRESERVICEROTA_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up FireServiceRota sensor based on a config entry.""" + coordinator = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id] + + async_add_entities([IncidentsSensor(coordinator)]) + + +class IncidentsSensor(RestoreEntity): + """Representation of FireServiceRota incidents sensor.""" + + def __init__(self, coordinator): + """Initialize.""" + self._coordinator = coordinator + self._entry_id = self._coordinator._entry.entry_id + self._unique_id = f"{self._coordinator._entry.unique_id}_Incidents" + self._state = None + self._state_attributes = {} + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return "Incidents" + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + if ( + "prio" in self._state_attributes + and self._state_attributes["prio"][0] == "a" + ): + return "mdi:ambulance" + + return "mdi:fire-truck" + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self._state + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor.""" + return self._unique_id + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def device_state_attributes(self) -> object: + """Return available attributes for sensor.""" + attr = {} + data = self._state_attributes + + if not data: + return attr + + for value in ( + "trigger", + "created_at", + "message_to_speech_url", + "prio", + "type", + "responder_mode", + "can_respond_until", + ): + if data.get(value): + attr[value] = data[value] + + if "address" not in data: + continue + + for address_value in ( + "latitude", + "longitude", + "address_type", + "formatted_address", + ): + if address_value in data["address"]: + attr[address_value] = data["address"][address_value] + + return attr + + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + await super().async_added_to_hass() + + state = await self.async_get_last_state() + if state: + self._state = state.state + self._state_attributes = state.attributes + _LOGGER.debug("Restored entity 'Incidents' state to: %s", self._state) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update", + self.coordinator_update, + ) + ) + + @callback + def coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + data = self._coordinator.websocket.incident_data() + if not data or "body" not in data: + return + + self._state = data["body"] + self._state_attributes = data + self.async_write_ha_state() diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json new file mode 100644 index 00000000000..c44673d6c2c --- /dev/null +++ b/homeassistant/components/fireservicerota/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "url": "Website" + } + }, + "reauth": { + "description": "Authentication tokens baceame invalid, login to recreate them.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json new file mode 100644 index 00000000000..fabf02b3002 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "FireServiceRota", + "data": { + "password": "Password", + "username": "Username", + "url": "Website" + } + }, + "reauth": { + "description": "Authentication tokens became invalid, login to recreate them.", + "data": { + "password": "Password" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication." + }, + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "create_entry": { + "default": "Successfully authenticated" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 02fdac9ed3e..a0d9cc2dd79 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -58,6 +58,7 @@ FLOWS = [ "enocean", "epson", "esphome", + "fireservicerota", "flick_electric", "flo", "flume", diff --git a/requirements_all.txt b/requirements_all.txt index 024e15f509a..30904d7520e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1381,6 +1381,9 @@ pyeverlights==0.1.0 # homeassistant.components.fido pyfido==2.1.1 +# homeassistant.components.fireservicerota +pyfireservicerota==0.0.40 + # homeassistant.components.flexit pyflexit==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94b279722e0..862157e45ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -684,6 +684,9 @@ pyeverlights==0.1.0 # homeassistant.components.fido pyfido==2.1.1 +# homeassistant.components.fireservicerota +pyfireservicerota==0.0.40 + # homeassistant.components.flume pyflume==0.5.5 diff --git a/tests/components/fireservicerota/__init__.py b/tests/components/fireservicerota/__init__.py new file mode 100644 index 00000000000..37e5364d782 --- /dev/null +++ b/tests/components/fireservicerota/__init__.py @@ -0,0 +1 @@ +"""Tests for the FireServiceRota integration.""" diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py new file mode 100644 index 00000000000..b826e6b303b --- /dev/null +++ b/tests/components/fireservicerota/test_config_flow.py @@ -0,0 +1,114 @@ +"""Test the FireServiceRota config flow.""" +from pyfireservicerota import InvalidAuthError + +from homeassistant import data_entry_flow +from homeassistant.components.fireservicerota.const import ( # pylint: disable=unused-import + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +MOCK_CONF = { + CONF_USERNAME: "my@email.address", + CONF_PASSWORD: "mypassw0rd", + CONF_URL: "www.brandweerrooster.nl", +} + + +MOCK_DATA = { + "auth_implementation": DOMAIN, + CONF_URL: MOCK_CONF[CONF_URL], + CONF_USERNAME: MOCK_CONF[CONF_USERNAME], + "token": { + "access_token": "test-access-token", + "token_type": "Bearer", + "expires_in": 1234, + "refresh_token": "test-refresh-token", + "created_at": 4321, + }, +} + +MOCK_TOKEN_INFO = { + "access_token": "test-access-token", + "token_type": "Bearer", + "expires_in": 1234, + "refresh_token": "test-refresh-token", + "created_at": 4321, +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_abort_if_already_setup(hass): + """Test abort if already setup.""" + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_USERNAME] + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + + with patch( + "homeassistant.components.fireservicerota.FireServiceRota.request_tokens", + side_effect=InvalidAuthError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_step_user(hass): + """Test the start of the config flow.""" + + with patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as MockFireServiceRota, patch( + "homeassistant.components.fireservicerota.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.fireservicerota.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + mock_fireservicerota = MockFireServiceRota.return_value + mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_CONF[CONF_USERNAME] + assert result["data"] == { + "auth_implementation": "fireservicerota", + CONF_URL: "www.brandweerrooster.nl", + CONF_USERNAME: "my@email.address", + "token": { + "access_token": "test-access-token", + "token_type": "Bearer", + "expires_in": 1234, + "refresh_token": "test-refresh-token", + "created_at": 4321, + }, + } + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From ac551179ae51ed43ca0963c253a954eee13b2452 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 25 Nov 2020 17:08:00 +0100 Subject: [PATCH 235/430] Fix flapping derivative tests where time would move between state changes (#43579) --- tests/components/derivative/test_sensor.py | 44 +++++++++++++--------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 96fee84126a..58090f1587d 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -23,11 +23,13 @@ async def test_state(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1, {}) - await hass.async_block_till_done() + base = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow") as now: + now.return_value = base + hass.states.async_set(entity_id, 1, {}) + await hass.async_block_till_done() - now = dt_util.utcnow() + timedelta(seconds=3600) - with patch("homeassistant.util.dt.utcnow", return_value=now): + now.return_value += timedelta(seconds=3600) hass.states.async_set(entity_id, 1, {}, force_update=True) await hass.async_block_till_done() @@ -63,9 +65,10 @@ async def setup_tests(hass, config, times, values, expected_state): config, entity_id = await _setup_sensor(hass, config) # Testing a energy sensor with non-monotonic intervals and values - for time, value in zip(times, values): - now = dt_util.utcnow() + timedelta(seconds=time) - with patch("homeassistant.util.dt.utcnow", return_value=now): + base = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow") as now: + for time, value in zip(times, values): + now.return_value = base + timedelta(seconds=time) hass.states.async_set(entity_id, value, {}, force_update=True) await hass.async_block_till_done() @@ -163,8 +166,9 @@ async def test_data_moving_average_for_discrete_sensor(hass): }, ) # two minute window + base = dt_util.utcnow() for time, value in zip(times, temperature_values): - now = dt_util.utcnow() + timedelta(seconds=time) + now = base + timedelta(seconds=time) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set(entity_id, value, {}, force_update=True) await hass.async_block_till_done() @@ -192,13 +196,15 @@ async def test_prefix(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True - ) - await hass.async_block_till_done() + base = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow") as now: + now.return_value = base + hass.states.async_set( + entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + ) + await hass.async_block_till_done() - now = dt_util.utcnow() + timedelta(seconds=3600) - with patch("homeassistant.util.dt.utcnow", return_value=now): + now.return_value += timedelta(seconds=3600) hass.states.async_set( entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True ) @@ -228,11 +234,13 @@ async def test_suffix(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1000, {}) - await hass.async_block_till_done() + base = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow") as now: + now.return_value = base + hass.states.async_set(entity_id, 1000, {}) + await hass.async_block_till_done() - now = dt_util.utcnow() + timedelta(seconds=10) - with patch("homeassistant.util.dt.utcnow", return_value=now): + now.return_value += timedelta(seconds=10) hass.states.async_set(entity_id, 1000, {}, force_update=True) await hass.async_block_till_done() From 314497d013fc3061c8b5154f512198ef3b97848e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 Nov 2020 17:40:32 +0100 Subject: [PATCH 236/430] Add Tasmota fan (#43353) * Add Tasmota fan * Bump hatasmota to 0.1.0 * Apply suggestions from code review --- homeassistant/components/tasmota/const.py | 1 + homeassistant/components/tasmota/fan.py | 88 ++++++++ tests/components/tasmota/test_fan.py | 255 ++++++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 homeassistant/components/tasmota/fan.py create mode 100644 tests/components/tasmota/test_fan.py diff --git a/homeassistant/components/tasmota/const.py b/homeassistant/components/tasmota/const.py index 0f4dfde1646..d1cdcca1db1 100644 --- a/homeassistant/components/tasmota/const.py +++ b/homeassistant/components/tasmota/const.py @@ -10,6 +10,7 @@ DOMAIN = "tasmota" PLATFORMS = [ "binary_sensor", + "fan", "light", "sensor", "switch", diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py new file mode 100644 index 00000000000..34aca5fb0ae --- /dev/null +++ b/homeassistant/components/tasmota/fan.py @@ -0,0 +1,88 @@ +"""Support for Tasmota fans.""" + +from hatasmota import const as tasmota_const + +from homeassistant.components import fan +from homeassistant.components.fan import FanEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW +from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate + +HA_TO_TASMOTA_SPEED_MAP = { + fan.SPEED_OFF: tasmota_const.FAN_SPEED_OFF, + fan.SPEED_LOW: tasmota_const.FAN_SPEED_LOW, + fan.SPEED_MEDIUM: tasmota_const.FAN_SPEED_MEDIUM, + fan.SPEED_HIGH: tasmota_const.FAN_SPEED_HIGH, +} + +TASMOTA_TO_HA_SPEED_MAP = {v: k for k, v in HA_TO_TASMOTA_SPEED_MAP.items()} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Tasmota fan dynamically through discovery.""" + + @callback + def async_discover(tasmota_entity, discovery_hash): + """Discover and add a Tasmota fan.""" + async_add_entities( + [TasmotaFan(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] + ) + + hass.data[ + DATA_REMOVE_DISCOVER_COMPONENT.format(fan.DOMAIN) + ] = async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(fan.DOMAIN, TASMOTA_DOMAIN), + async_discover, + ) + + +class TasmotaFan( + TasmotaAvailability, + TasmotaDiscoveryUpdate, + FanEntity, +): + """Representation of a Tasmota fan.""" + + def __init__(self, **kwds): + """Initialize the Tasmota fan.""" + self._state = None + + super().__init__( + discovery_update=self.discovery_update, + **kwds, + ) + + @property + def speed(self): + """Return the current speed.""" + return TASMOTA_TO_HA_SPEED_MAP.get(self._state) + + @property + def speed_list(self): + """Get the list of available speeds.""" + return list(HA_TO_TASMOTA_SPEED_MAP.keys()) + + @property + def supported_features(self): + """Flag supported features.""" + return fan.SUPPORT_SET_SPEED + + async def async_set_speed(self, speed): + """Set the speed of the fan.""" + if speed == fan.SPEED_OFF: + await self.async_turn_off() + else: + self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed]) + + async def async_turn_on(self, speed=None, **kwargs): + """Turn the fan on.""" + # Tasmota does not support turning a fan on with implicit speed + await self.async_set_speed(speed or fan.SPEED_MEDIUM) + + async def async_turn_off(self, **kwargs): + """Turn the fan off.""" + self._tasmota_entity.set_speed(tasmota_const.FAN_SPEED_OFF) diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py new file mode 100644 index 00000000000..5cadc20218e --- /dev/null +++ b/tests/components/tasmota/test_fan.py @@ -0,0 +1,255 @@ +"""The tests for the Tasmota fan platform.""" +import copy +import json + +from hatasmota.utils import ( + get_topic_stat_result, + get_topic_tele_state, + get_topic_tele_will, +) + +from homeassistant.components import fan +from homeassistant.components.tasmota.const import DEFAULT_PREFIX +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON + +from .test_common import ( + DEFAULT_CONFIG, + help_test_availability, + help_test_availability_discovery_update, + help_test_availability_poll_state, + help_test_availability_when_connection_lost, + help_test_discovery_device_remove, + help_test_discovery_removal, + help_test_discovery_update_unchanged, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, +) + +from tests.async_mock import patch +from tests.common import async_fire_mqtt_message +from tests.components.fan import common + + +async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("fan.tasmota") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + assert state.attributes["speed"] is None + assert state.attributes["speed_list"] == ["off", "low", "medium", "high"] + assert state.attributes["supported_features"] == fan.SUPPORT_SET_SPEED + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":1}') + state = hass.states.get("fan.tasmota") + assert state.state == STATE_ON + assert state.attributes["speed"] == "low" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":2}') + state = hass.states.get("fan.tasmota") + assert state.state == STATE_ON + assert state.attributes["speed"] == "medium" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":3}') + state = hass.states.get("fan.tasmota") + assert state.state == STATE_ON + assert state.attributes["speed"] == "high" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":0}') + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + assert state.attributes["speed"] == "off" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":1}') + state = hass.states.get("fan.tasmota") + assert state.state == STATE_ON + assert state.attributes["speed"] == "low" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":0}') + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + assert state.attributes["speed"] == "off" + + +async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Turn the fan on and verify MQTT message is sent + await common.async_turn_on(hass, "fan.tasmota") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + + # Turn the fan off and verify MQTT message is sent + await common.async_turn_off(hass, "fan.tasmota") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed and verify MQTT message is sent + await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_OFF) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed and verify MQTT message is sent + await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_LOW) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "1", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed and verify MQTT message is sent + await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_MEDIUM) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed and verify MQTT message is sent + await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_HIGH) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False + ) + + +async def test_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + await help_test_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, fan.DOMAIN, config + ) + + +async def test_availability(hass, mqtt_mock, setup_tasmota): + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + await help_test_availability(hass, mqtt_mock, fan.DOMAIN, config) + + +async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): + """Test availability discovery update.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + await help_test_availability_discovery_update(hass, mqtt_mock, fan.DOMAIN, config) + + +async def test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test polling after MQTT connection (re)established.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + poll_topic = "tasmota_49A3BC/cmnd/STATE" + await help_test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, fan.DOMAIN, config, poll_topic, "" + ) + + +async def test_discovery_removal_fan(hass, mqtt_mock, caplog, setup_tasmota): + """Test removal of discovered fan.""" + config1 = copy.deepcopy(DEFAULT_CONFIG) + config1["dn"] = "Test" + config1["if"] = 1 + config2 = copy.deepcopy(DEFAULT_CONFIG) + config2["dn"] = "Test" + config2["if"] = 0 + + await help_test_discovery_removal( + hass, mqtt_mock, caplog, fan.DOMAIN, config1, config2 + ) + + +async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog, setup_tasmota): + """Test update of discovered fan.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + with patch( + "homeassistant.components.tasmota.fan.TasmotaFan.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, fan.DOMAIN, config, discovery_update + ) + + +async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): + """Test device registry remove.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + unique_id = f"{DEFAULT_CONFIG['mac']}_fan_fan_ifan" + await help_test_discovery_device_remove( + hass, mqtt_mock, fan.DOMAIN, unique_id, config + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): + """Test MQTT subscriptions are managed when entity_id is updated.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + topics = [ + get_topic_stat_result(config), + get_topic_tele_state(config), + get_topic_tele_will(config), + ] + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, fan.DOMAIN, config, topics + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota): + """Test MQTT discovery update when entity_id is updated.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["if"] = 1 + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, fan.DOMAIN, config + ) From 6706ea36de46235bd9f162dc80fd711f52c2628c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 Nov 2020 18:52:09 +0100 Subject: [PATCH 237/430] Add Tasmota cover (#43368) * Add Tasmota cover * Update tests * Bump hatasmota to 0.1.0 --- homeassistant/components/tasmota/const.py | 1 + homeassistant/components/tasmota/cover.py | 108 ++++ tests/components/tasmota/test_cover.py | 629 ++++++++++++++++++++++ 3 files changed, 738 insertions(+) create mode 100644 homeassistant/components/tasmota/cover.py create mode 100644 tests/components/tasmota/test_cover.py diff --git a/homeassistant/components/tasmota/const.py b/homeassistant/components/tasmota/const.py index d1cdcca1db1..48026f3e93d 100644 --- a/homeassistant/components/tasmota/const.py +++ b/homeassistant/components/tasmota/const.py @@ -10,6 +10,7 @@ DOMAIN = "tasmota" PLATFORMS = [ "binary_sensor", + "cover", "fan", "light", "sensor", diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py new file mode 100644 index 00000000000..3556f6ebf52 --- /dev/null +++ b/homeassistant/components/tasmota/cover.py @@ -0,0 +1,108 @@ +"""Support for Tasmota covers.""" + +from hatasmota import const as tasmota_const + +from homeassistant.components import cover +from homeassistant.components.cover import CoverEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW +from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Tasmota cover dynamically through discovery.""" + + @callback + def async_discover(tasmota_entity, discovery_hash): + """Discover and add a Tasmota cover.""" + async_add_entities( + [TasmotaCover(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] + ) + + hass.data[ + DATA_REMOVE_DISCOVER_COMPONENT.format(cover.DOMAIN) + ] = async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(cover.DOMAIN, TASMOTA_DOMAIN), + async_discover, + ) + + +class TasmotaCover( + TasmotaAvailability, + TasmotaDiscoveryUpdate, + CoverEntity, +): + """Representation of a Tasmota cover.""" + + def __init__(self, **kwds): + """Initialize the Tasmota cover.""" + self._direction = None + self._position = None + + super().__init__( + discovery_update=self.discovery_update, + **kwds, + ) + + @callback + def state_updated(self, state, **kwargs): + """Handle state updates.""" + self._direction = kwargs["direction"] + self._position = kwargs["position"] + self.async_write_ha_state() + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._position + + @property + def supported_features(self): + """Flag supported features.""" + return ( + cover.SUPPORT_OPEN + | cover.SUPPORT_CLOSE + | cover.SUPPORT_STOP + | cover.SUPPORT_SET_POSITION + ) + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._direction == tasmota_const.SHUTTER_DIRECTION_UP + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._direction == tasmota_const.SHUTTER_DIRECTION_DOWN + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._position is None: + return None + return self._position == 0 + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._tasmota_entity.open() + + async def async_close_cover(self, **kwargs): + """Close cover.""" + self._tasmota_entity.close() + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[cover.ATTR_POSITION] + self._tasmota_entity.set_position(position) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + self._tasmota_entity.stop() diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py new file mode 100644 index 00000000000..d5ae01f1666 --- /dev/null +++ b/tests/components/tasmota/test_cover.py @@ -0,0 +1,629 @@ +"""The tests for the Tasmota cover platform.""" +import copy +import json + +from hatasmota.utils import ( + get_topic_stat_result, + get_topic_stat_status, + get_topic_tele_sensor, + get_topic_tele_will, +) + +from homeassistant.components import cover +from homeassistant.components.tasmota.const import DEFAULT_PREFIX +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN + +from .test_common import ( + DEFAULT_CONFIG, + help_test_availability, + help_test_availability_discovery_update, + help_test_availability_poll_state, + help_test_availability_when_connection_lost, + help_test_discovery_device_remove, + help_test_discovery_removal, + help_test_discovery_update_unchanged, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, +) + +from tests.async_mock import patch +from tests.common import async_fire_mqtt_message + + +async def test_missing_relay(hass, mqtt_mock, setup_tasmota): + """Test no cover is discovered if relays are missing.""" + + +async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 3 + config["rl"][1] = 3 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == STATE_UNKNOWN + assert ( + state.attributes["supported_features"] + == cover.SUPPORT_OPEN + | cover.SUPPORT_CLOSE + | cover.SUPPORT_STOP + | cover.SUPPORT_SET_POSITION + ) + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Periodic updates + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":54,"Direction":-1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closing" + assert state.attributes["current_position"] == 54 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":100,"Direction":1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "opening" + assert state.attributes["current_position"] == 100 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closed" + assert state.attributes["current_position"] == 0 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":1,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 1 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":100,"Direction":0}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 100 + + # State poll response + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closing" + assert state.attributes["current_position"] == 54 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "opening" + assert state.attributes["current_position"] == 100 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closed" + assert state.attributes["current_position"] == 0 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":1,"Direction":0}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 1 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 100 + + # Command response + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":54,"Direction":-1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closing" + assert state.attributes["current_position"] == 54 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":100,"Direction":1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "opening" + assert state.attributes["current_position"] == 100 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closed" + assert state.attributes["current_position"] == 0 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 1 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":100,"Direction":0}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 100 + + +async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 3 + config["rl"][1] = 3 + config["sho"] = [1] # Inverted cover + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == STATE_UNKNOWN + assert ( + state.attributes["supported_features"] + == cover.SUPPORT_OPEN + | cover.SUPPORT_CLOSE + | cover.SUPPORT_STOP + | cover.SUPPORT_SET_POSITION + ) + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Periodic updates + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":54,"Direction":-1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "opening" + assert state.attributes["current_position"] == 46 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":100,"Direction":1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closing" + assert state.attributes["current_position"] == 0 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 100 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":99,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 1 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":100,"Direction":0}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closed" + assert state.attributes["current_position"] == 0 + + # State poll response + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "opening" + assert state.attributes["current_position"] == 46 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closing" + assert state.attributes["current_position"] == 0 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 100 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":99,"Direction":0}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 1 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0}}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closed" + assert state.attributes["current_position"] == 0 + + # Command response + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":54,"Direction":-1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "opening" + assert state.attributes["current_position"] == 46 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":100,"Direction":1}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closing" + assert state.attributes["current_position"] == 0 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 100 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}' + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "open" + assert state.attributes["current_position"] == 99 + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":100,"Direction":0}}', + ) + state = hass.states.get("cover.tasmota_cover_1") + assert state.state == "closed" + assert state.attributes["current_position"] == 0 + + +async def call_service(hass, entity_id, service, **kwargs): + """Call a fan service.""" + await hass.services.async_call( + cover.DOMAIN, + service, + {"entity_id": entity_id, **kwargs}, + blocking=True, + ) + + +async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("cover.test_cover_1") + assert state.state == STATE_UNKNOWN + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Close the cover and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "close_cover") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterClose1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be unknown + state = hass.states.get("cover.test_cover_1") + assert state.state == STATE_UNKNOWN + + # Open the cover and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "open_cover") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterOpen1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Stop the cover and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "stop_cover") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterStop1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set position and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "set_cover_position", position=0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterPosition1", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set position and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "set_cover_position", position=99) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterPosition1", "99", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_sending_mqtt_commands_inverted(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + config["sho"] = [1] # Inverted cover + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("cover.test_cover_1") + assert state.state == STATE_UNKNOWN + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Close the cover and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "close_cover") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterClose1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be unknown + state = hass.states.get("cover.test_cover_1") + assert state.state == STATE_UNKNOWN + + # Open the cover and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "open_cover") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterOpen1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Stop the cover and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "stop_cover") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterStop1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set position and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "set_cover_position", position=0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterPosition1", "100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set position and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "set_cover_position", position=99) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterPosition1", "1", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + cover.DOMAIN, + config, + entity_id="test_cover_1", + ) + + +async def test_availability(hass, mqtt_mock, setup_tasmota): + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_availability( + hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1" + ) + + +async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): + """Test availability discovery update.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_availability_discovery_update( + hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1" + ) + + +async def test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test polling after MQTT connection (re)established.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 3 + config["rl"][1] = 3 + poll_topic = "tasmota_49A3BC/cmnd/STATUS" + await help_test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, cover.DOMAIN, config, poll_topic, "10" + ) + + +async def test_discovery_removal_cover(hass, mqtt_mock, caplog, setup_tasmota): + """Test removal of discovered cover.""" + config1 = copy.deepcopy(DEFAULT_CONFIG) + config1["dn"] = "Test" + config1["rl"][0] = 3 + config1["rl"][1] = 3 + config2 = copy.deepcopy(DEFAULT_CONFIG) + config2["dn"] = "Test" + config2["rl"][0] = 0 + config2["rl"][1] = 0 + + await help_test_discovery_removal( + hass, + mqtt_mock, + caplog, + cover.DOMAIN, + config1, + config2, + entity_id="test_cover_1", + name="Test cover 1", + ) + + +async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog, setup_tasmota): + """Test update of discovered cover.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + with patch( + "homeassistant.components.tasmota.cover.TasmotaCover.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, + mqtt_mock, + caplog, + cover.DOMAIN, + config, + discovery_update, + entity_id="test_cover_1", + name="Test cover 1", + ) + + +async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): + """Test device registry remove.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + unique_id = f"{DEFAULT_CONFIG['mac']}_cover_shutter_0" + await help_test_discovery_device_remove( + hass, mqtt_mock, cover.DOMAIN, unique_id, config + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): + """Test MQTT subscriptions are managed when entity_id is updated.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + topics = [ + get_topic_stat_result(config), + get_topic_tele_sensor(config), + get_topic_stat_status(config, 10), + get_topic_tele_will(config), + ] + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, cover.DOMAIN, config, topics, entity_id="test_cover_1" + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota): + """Test MQTT discovery update when entity_id is updated.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1" + ) From 3767af14f3ca09a11a0b06292a8caf67fac496a4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Nov 2020 20:03:56 +0100 Subject: [PATCH 238/430] Add more selectors (#43639) --- .../components/alexa/capabilities.py | 2 +- .../components/input_number/__init__.py | 10 ++--- homeassistant/helpers/selector.py | 37 ++++++++++++++++++ tests/helpers/test_selector.py | 39 +++++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ac4784132f5..008870c8dd9 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1564,7 +1564,7 @@ class AlexaRangeController(AlexaCapability): 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) + unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._resource = AlexaPresetResource( ["Value", AlexaGlobalCatalog.SETTING_PRESET], diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 8ec26ea3956..1f979cad7a9 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -7,11 +7,11 @@ import voluptuous as vol from homeassistant.const import ( ATTR_EDITABLE, ATTR_MODE, - ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_ID, CONF_MODE, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) from homeassistant.core import callback @@ -67,7 +67,7 @@ CREATE_FIELDS = { vol.Optional(CONF_INITIAL): vol.Coerce(float), vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), vol.Range(min=1e-3)), vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]), } @@ -78,7 +78,7 @@ UPDATE_FIELDS = { vol.Optional(CONF_INITIAL): vol.Coerce(float), vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-3)), vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]), } @@ -95,7 +95,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Coerce(float), vol.Range(min=1e-3) ), vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In( [MODE_BOX, MODE_SLIDER] ), @@ -250,7 +250,7 @@ class InputNumber(RestoreEntity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._config.get(ATTR_UNIT_OF_MEASUREMENT) + return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property def unique_id(self) -> typing.Optional[str]: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index d3e96a5205e..d02a41f5f97 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, cast import voluptuous as vol +from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.util import decorator SELECTORS = decorator.Registry() @@ -68,3 +69,39 @@ class DeviceSelector(Selector): vol.Optional("model"): str, } ) + + +@SELECTORS.register("number") +class NumberSelector(Selector): + """Selector of a numeric value.""" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("min"): vol.Coerce(float), + vol.Required("max"): vol.Coerce(float), + vol.Optional("step", default=1): vol.All( + vol.Coerce(float), vol.Range(min=1e-3) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, + vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]), + } + ) + + +@SELECTORS.register("boolean") +class BooleanSelector(Selector): + """Selector of a boolean value.""" + + CONFIG_SCHEMA = vol.Schema({}) + + +@SELECTORS.register("datetime") +class DateTimeSelector(Selector): + """Selector of a date and or time value.""" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("has_date", default=False): bool, + vol.Optional("has_time", default=False): bool, + } + ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index a8410821c42..c5b11285e48 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -66,3 +66,42 @@ def test_device_selector_schema(schema): def test_entity_selector_schema(schema): """Test entity selector.""" selector.validate_selector({"entity": schema}) + + +@pytest.mark.parametrize( + "schema", + ( + {"min": 10, "max": 50}, + {"min": -100, "max": 100, "step": 5}, + {"min": -20, "max": -10, "mode": "box"}, + {"min": 0, "max": 100, "unit_of_measurement": "seconds", "mode": "slider"}, + {"min": 10, "max": 1000, "mode": "slider", "step": 0.5}, + ), +) +def test_number_selector_schema(schema): + """Test number selector.""" + selector.validate_selector({"number": schema}) + + +@pytest.mark.parametrize( + "schema", + ({},), +) +def test_boolean_selector_schema(schema): + """Test boolean selector.""" + selector.validate_selector({"boolean": schema}) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"has_date": True, "has_time": True}, + {"has_date": False, "has_time": False}, + {"has_date": True, "has_time": False}, + {"has_date": False, "has_time": True}, + ), +) +def test_datetime_selector_schema(schema): + """Test datetime selector.""" + selector.validate_selector({"datetime": schema}) From 8533d9cae0cc118bc03fc7a9c20dd059a0f929db Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Nov 2020 20:05:43 +0100 Subject: [PATCH 239/430] Add default to inputs (#43636) --- homeassistant/components/blueprint/models.py | 33 ++++++++++- homeassistant/components/blueprint/schemas.py | 9 ++- tests/components/blueprint/test_models.py | 58 +++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index e19439133ee..85c87b5baa7 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -10,7 +10,13 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import loader -from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, __version__ +from homeassistant.const import ( + CONF_DEFAULT, + CONF_DOMAIN, + CONF_NAME, + CONF_PATH, + __version__, +) from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import placeholder @@ -82,6 +88,11 @@ class Blueprint: """Return blueprint name.""" return self.data[CONF_BLUEPRINT][CONF_NAME] + @property + def inputs(self) -> dict: + """Return blueprint inputs.""" + return self.data[CONF_BLUEPRINT][CONF_INPUT] + @property def metadata(self) -> dict: """Return blueprint metadata.""" @@ -129,9 +140,23 @@ class BlueprintInputs: """Return the inputs.""" return self.config_with_inputs[CONF_USE_BLUEPRINT][CONF_INPUT] + @property + def inputs_with_default(self): + """Return the inputs and fallback to defaults.""" + no_input = self.blueprint.placeholders - set(self.inputs) + + inputs_with_default = dict(self.inputs) + + for inp in no_input: + blueprint_input = self.blueprint.inputs[inp] + if isinstance(blueprint_input, dict) and CONF_DEFAULT in blueprint_input: + inputs_with_default[inp] = blueprint_input[CONF_DEFAULT] + + return inputs_with_default + def validate(self) -> None: """Validate the inputs.""" - missing = self.blueprint.placeholders - set(self.inputs) + missing = self.blueprint.placeholders - set(self.inputs_with_default) if missing: raise MissingPlaceholder( @@ -144,7 +169,9 @@ class BlueprintInputs: @callback def async_substitute(self) -> dict: """Get the blueprint value with the inputs substituted.""" - processed = placeholder.substitute(self.blueprint.data, self.inputs) + processed = placeholder.substitute( + self.blueprint.data, self.inputs_with_default + ) combined = {**self.config_with_inputs, **processed} # From config_with_inputs combined.pop(CONF_USE_BLUEPRINT) diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index 900497c3f71..07d8e8b0128 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -3,7 +3,13 @@ from typing import Any import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, CONF_SELECTOR +from homeassistant.const import ( + CONF_DEFAULT, + CONF_DOMAIN, + CONF_NAME, + CONF_PATH, + CONF_SELECTOR, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, selector @@ -54,6 +60,7 @@ BLUEPRINT_INPUT_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): str, vol.Optional(CONF_DESCRIPTION): str, + vol.Optional(CONF_DEFAULT): cv.match_all, vol.Optional(CONF_SELECTOR): selector.validate_selector, } ) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 48fbf617fa1..5c2d5f965ff 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -27,6 +27,26 @@ def blueprint_1(): ) +@pytest.fixture +def blueprint_2(): + """Blueprint fixture with default placeholder.""" + return models.Blueprint( + { + "blueprint": { + "name": "Hello", + "domain": "automation", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + "input": { + "test-placeholder": {"name": "Name", "description": "Description"}, + "test-placeholder-default": {"default": "test"}, + }, + }, + "example": Placeholder("test-placeholder"), + "example-default": Placeholder("test-placeholder-default"), + } + ) + + @pytest.fixture def domain_bps(hass): """Domain blueprints fixture.""" @@ -134,6 +154,44 @@ def test_blueprint_inputs_validation(blueprint_1): inputs.validate() +def test_blueprint_inputs_default(blueprint_2): + """Test blueprint inputs.""" + inputs = models.BlueprintInputs( + blueprint_2, + {"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}}, + ) + inputs.validate() + assert inputs.inputs == {"test-placeholder": 1} + assert inputs.inputs_with_default == { + "test-placeholder": 1, + "test-placeholder-default": "test", + } + assert inputs.async_substitute() == {"example": 1, "example-default": "test"} + + +def test_blueprint_inputs_override_default(blueprint_2): + """Test blueprint inputs.""" + inputs = models.BlueprintInputs( + blueprint_2, + { + "use_blueprint": { + "path": "bla", + "input": {"test-placeholder": 1, "test-placeholder-default": "custom"}, + } + }, + ) + inputs.validate() + assert inputs.inputs == { + "test-placeholder": 1, + "test-placeholder-default": "custom", + } + assert inputs.inputs_with_default == { + "test-placeholder": 1, + "test-placeholder-default": "custom", + } + assert inputs.async_substitute() == {"example": 1, "example-default": "custom"} + + async def test_domain_blueprints_get_blueprint_errors(hass, domain_bps): """Test domain blueprints.""" assert hass.data["blueprint"]["automation"] is domain_bps From c439d25ac387f30e5e1f0d996784a589c7f398fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Wed, 25 Nov 2020 21:41:56 +0100 Subject: [PATCH 240/430] Add location to summary sensor attributes (#43641) --- homeassistant/components/seventeentrack/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 7abed897346..94efe9b98c7 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -152,6 +152,7 @@ class SeventeenTrackSummarySensor(Entity): ATTR_FRIENDLY_NAME: package.friendly_name, ATTR_INFO_TEXT: package.info_text, ATTR_STATUS: package.status, + ATTR_LOCATION: package.location, ATTR_TRACKING_NUMBER: package.tracking_number, } ) From ed16c5078ff4b2351f0d55237d962e4fb17d39f0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 26 Nov 2020 00:03:15 +0000 Subject: [PATCH 241/430] [ci skip] Translation update --- .../ambiclimate/translations/ka.json | 8 ++++ .../azure_devops/translations/ka.json | 8 ++++ .../cloudflare/translations/ka.json | 5 +++ .../components/deconz/translations/ka.json | 7 ++++ .../components/denonavr/translations/ka.json | 7 ++++ .../emulated_roku/translations/ka.json | 7 ++++ .../fireservicerota/translations/ca.json | 29 +++++++++++++++ .../fireservicerota/translations/en.json | 37 +++++++++---------- .../fireservicerota/translations/pl.json | 29 +++++++++++++++ .../fireservicerota/translations/ru.json | 29 +++++++++++++++ .../geonetnz_volcano/translations/ka.json | 7 ++++ .../homeassistant/translations/ka.json | 1 + .../components/kodi/translations/ka.json | 7 ++++ .../components/life360/translations/ka.json | 2 + .../components/mill/translations/ka.json | 7 ++++ .../motion_blinds/translations/ka.json | 20 ++++++++++ .../components/nest/translations/cs.json | 3 +- .../components/nest/translations/es.json | 3 +- .../components/nest/translations/et.json | 3 +- .../components/nest/translations/ka.json | 11 +++++- .../components/nest/translations/no.json | 3 +- .../components/nest/translations/zh-Hant.json | 3 +- .../ovo_energy/translations/ka.json | 17 +++++++++ .../components/point/translations/cs.json | 3 +- .../components/point/translations/es.json | 3 +- .../components/point/translations/et.json | 3 +- .../components/point/translations/ka.json | 7 ++++ .../components/point/translations/no.json | 3 +- .../point/translations/zh-Hant.json | 3 +- .../components/sharkiq/translations/ka.json | 7 ++++ .../simplisafe/translations/ka.json | 7 ++++ .../components/solaredge/translations/cs.json | 3 +- .../components/solaredge/translations/ka.json | 13 +++++++ .../srp_energy/translations/ka.json | 1 + .../tellduslive/translations/cs.json | 3 +- .../tellduslive/translations/es.json | 3 +- .../tellduslive/translations/et.json | 3 +- .../tellduslive/translations/ka.json | 7 ++++ .../tellduslive/translations/no.json | 3 +- .../tellduslive/translations/zh-Hant.json | 3 +- .../components/toon/translations/cs.json | 3 +- .../components/toon/translations/es.json | 3 +- .../components/toon/translations/et.json | 3 +- .../components/toon/translations/ka.json | 7 ++++ .../components/toon/translations/no.json | 3 +- .../components/toon/translations/zh-Hant.json | 3 +- .../water_heater/translations/ka.json | 8 ++++ 47 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/ambiclimate/translations/ka.json create mode 100644 homeassistant/components/azure_devops/translations/ka.json create mode 100644 homeassistant/components/deconz/translations/ka.json create mode 100644 homeassistant/components/denonavr/translations/ka.json create mode 100644 homeassistant/components/emulated_roku/translations/ka.json create mode 100644 homeassistant/components/fireservicerota/translations/ca.json create mode 100644 homeassistant/components/fireservicerota/translations/pl.json create mode 100644 homeassistant/components/fireservicerota/translations/ru.json create mode 100644 homeassistant/components/geonetnz_volcano/translations/ka.json create mode 100644 homeassistant/components/kodi/translations/ka.json create mode 100644 homeassistant/components/mill/translations/ka.json create mode 100644 homeassistant/components/motion_blinds/translations/ka.json create mode 100644 homeassistant/components/ovo_energy/translations/ka.json create mode 100644 homeassistant/components/point/translations/ka.json create mode 100644 homeassistant/components/sharkiq/translations/ka.json create mode 100644 homeassistant/components/simplisafe/translations/ka.json create mode 100644 homeassistant/components/solaredge/translations/ka.json create mode 100644 homeassistant/components/tellduslive/translations/ka.json create mode 100644 homeassistant/components/toon/translations/ka.json create mode 100644 homeassistant/components/water_heater/translations/ka.json diff --git a/homeassistant/components/ambiclimate/translations/ka.json b/homeassistant/components/ambiclimate/translations/ka.json new file mode 100644 index 00000000000..ed77d38ab45 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/ka.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "missing_configuration": "\u10d4\u10e1 \u10d9\u10dd\u10db\u10de\u10dd\u10dc\u10d4\u10dc\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10db\u10d8\u10e7\u10d5\u10d4\u10d7 \u10d3\u10dd\u10d9\u10e3\u10db\u10d4\u10dc\u10e2\u10d0\u10ea\u10d8\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/ka.json b/homeassistant/components/azure_devops/translations/ka.json new file mode 100644 index 00000000000..dd48647ed7b --- /dev/null +++ b/homeassistant/components/azure_devops/translations/ka.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/ka.json b/homeassistant/components/cloudflare/translations/ka.json index 349dbc81682..6ba93fd16ea 100644 --- a/homeassistant/components/cloudflare/translations/ka.json +++ b/homeassistant/components/cloudflare/translations/ka.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10d8.", + "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + }, "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", "invalid_auth": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", "invalid_zone": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d6\u10dd\u10dc\u10d0" }, diff --git a/homeassistant/components/deconz/translations/ka.json b/homeassistant/components/deconz/translations/ka.json new file mode 100644 index 00000000000..932e521a8ad --- /dev/null +++ b/homeassistant/components/deconz/translations/ka.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "remote_button_rotated_fast": "\u10e6\u10d8\u10da\u10d0\u10d9\u10d8 \u10e1\u10ec\u10e0\u10d0\u10e4\u10d0\u10d3 \u10d1\u10e0\u10e3\u10dc\u10d0\u10d5\u10e1 \" {subtype} \"" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ka.json b/homeassistant/components/denonavr/translations/ka.json new file mode 100644 index 00000000000..fbb3a50cb06 --- /dev/null +++ b/homeassistant/components/denonavr/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0, \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10e1\u10ea\u10d0\u10d3\u10dd\u10d7 \u10ee\u10d4\u10da\u10d0\u10ee\u10da\u10d0, \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10dd\u10d0 \u10d9\u10d5\u10d4\u10d1\u10d8\u10e1 \u10d3\u10d0 \u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10d9\u10d0\u10d1\u10d4\u10da\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10d7\u10d8\u10e8\u10d5\u10d0\u10db \u10d3\u10d0\u10d2\u10d4\u10ee\u10db\u10d0\u10e0\u10dd\u10d7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/ka.json b/homeassistant/components/emulated_roku/translations/ka.json new file mode 100644 index 00000000000..fb52156e9cf --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ca.json b/homeassistant/components/fireservicerota/translations/ca.json new file mode 100644 index 00000000000..287bb81e51e --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "reauth": { + "data": { + "password": "Contrasenya" + }, + "description": "Els tokens d'autenticaci\u00f3 ja no s\u00f3n v\u00e0lids, inicia sessi\u00f3 per tornar-los a generar." + }, + "user": { + "data": { + "password": "Contrasenya", + "url": "Lloc web", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json index fabf02b3002..288b89c31b8 100644 --- a/homeassistant/components/fireservicerota/translations/en.json +++ b/homeassistant/components/fireservicerota/translations/en.json @@ -1,30 +1,29 @@ { "config": { - "step": { - "user": { - "title": "FireServiceRota", - "data": { - "password": "Password", - "username": "Username", - "url": "Website" - } - }, - "reauth": { - "description": "Authentication tokens became invalid, login to recreate them.", - "data": { - "password": "Password" - } - } - }, - "error": { - "invalid_auth": "Invalid authentication." - }, "abort": { "already_configured": "Account is already configured", "reauth_successful": "Re-authentication was successful" }, "create_entry": { "default": "Successfully authenticated" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "Authentication tokens baceame invalid, login to recreate them." + }, + "user": { + "data": { + "password": "Password", + "url": "Website", + "username": "Username" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/pl.json b/homeassistant/components/fireservicerota/translations/pl.json new file mode 100644 index 00000000000..2e5e480fcc1 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "reauth": { + "data": { + "password": "Has\u0142o" + }, + "description": "Tokeny uwierzytelniaj\u0105ce straci\u0142y wa\u017cno\u015b\u0107. Zaloguj si\u0119, aby je odtworzy\u0107." + }, + "user": { + "data": { + "password": "Has\u0142o", + "url": "Strona internetowa", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json new file mode 100644 index 00000000000..2c90bd53ca9 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "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." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u043e\u043a\u0435\u043d\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b, \u0432\u043e\u0439\u0434\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0445 \u0437\u0430\u043d\u043e\u0432\u043e." + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/ka.json b/homeassistant/components/geonetnz_volcano/translations/ka.json new file mode 100644 index 00000000000..2354e938d34 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u10d0\u10d3\u10d2\u10d8\u10da\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10dd\u10d1\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ka.json b/homeassistant/components/homeassistant/translations/ka.json index 0acadcdf3a1..4b5dec2fd30 100644 --- a/homeassistant/components/homeassistant/translations/ka.json +++ b/homeassistant/components/homeassistant/translations/ka.json @@ -3,6 +3,7 @@ "info": { "arch": "\u10de\u10e0\u10dd\u10ea\u10d4\u10e1\u10dd\u10e0\u10d8\u10e1 \u10d0\u10e0\u10e5\u10d8\u10e2\u10d4\u10e5\u10e2\u10e3\u10e0\u10d0", "chassis": "\u10e8\u10d0\u10e1\u10d8", + "dev": "\u10e8\u10d4\u10db\u10e3\u10e8\u10d0\u10d5\u10d4\u10d1\u10d0", "docker": "Docker", "docker_version": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/kodi/translations/ka.json b/homeassistant/components/kodi/translations/ka.json new file mode 100644 index 00000000000..16977c0ec0c --- /dev/null +++ b/homeassistant/components/kodi/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_uuid": "\u10d9\u10dd\u10d3\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10dc\u10e1\u10e1 \u10d0\u10e0 \u10d0\u10e5\u10d5\u10e1 \u10e3\u10dc\u10d8\u10d9\u10d0\u10da\u10e3\u10e0\u10d8 ID. \u10e1\u10d0\u10d5\u10d0\u10e0\u10d0\u10e3\u10d3\u10dd\u10d3 \u10d4\u10e1 \u10d2\u10d0\u10db\u10dd\u10ec\u10d5\u10d4\u10e3\u10da\u10d8\u10d0 \u10eb\u10d5\u10d4\u10da\u10d8 Kodi \u10d5\u10d4\u10e0\u10e1\u10d8\u10d8\u10d7 (17.x \u10d0\u10dc \u10e5\u10d5\u10d4\u10db\u10dd\u10d7). \u10d7\u10e5\u10d5\u10d4\u10dc \u10e8\u10d4\u10d2\u10d8\u10eb\u10da\u10d8\u10d0\u10d7 \u10ee\u10d4\u10da\u10d8\u10d7 \u10db\u10dd\u10d0\u10ee\u10d3\u10d8\u10dc\u10dd\u10d7 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d0\u10dc \u10d2\u10d0\u10d3\u10d0\u10ee\u10d5\u10d8\u10d3\u10d4\u10d7 Kodi- \u10e1 \u10e3\u10d0\u10ee\u10da\u10d4\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0\u10d6\u10d4." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ka.json b/homeassistant/components/life360/translations/ka.json index 4069ab18e6f..35a27bfc78f 100644 --- a/homeassistant/components/life360/translations/ka.json +++ b/homeassistant/components/life360/translations/ka.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" }, "error": { "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" } } diff --git a/homeassistant/components/mill/translations/ka.json b/homeassistant/components/mill/translations/ka.json new file mode 100644 index 00000000000..e965142dbe1 --- /dev/null +++ b/homeassistant/components/mill/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ka.json b/homeassistant/components/motion_blinds/translations/ka.json new file mode 100644 index 00000000000..e8ea5e0deba --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/ka.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u10db\u10dd\u10ec\u10e7\u10de\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "already_in_progress": "\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10db\u10d3\u10d4\u10d5\u10e0\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4\u10dd\u10d1\u10e1", + "connection_error": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0" + }, + "flow_title": "\u10db\u10dd\u10eb\u10e0\u10d0\u10d5\u10d8 \u10df\u10d0\u10da\u10e3\u10d6\u10d4\u10d1\u10d8", + "step": { + "user": { + "data": { + "api_key": "API Key", + "host": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8" + }, + "description": "\u10d7\u10e5\u10d5\u10d4\u10dc \u10d3\u10d0\u10d2\u10ed\u10d8\u10e0\u10d3\u10d4\u10d1\u10d0\u10d7 16 \u10d0\u10e1\u10dd\u10d8\u10d0\u10dc\u10d8 API key, \u10d8\u10dc\u10e1\u10e2\u10e0\u10e3\u10e5\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d8\u10ee\u10d8\u10da\u10d4\u10d7 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "\u10db\u10dd\u10eb\u10e0\u10d0\u10d5\u10d8 \u10df\u10d0\u10da\u10e3\u10d6\u10d4\u10d1\u10d8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index 28268d9bab8..9c3ae442631 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", + "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 0c902ff89be..7db77ad954e 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { "default": "Autenticado con \u00e9xito" diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 37f7e26a885..2e131b6f9fe 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", + "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" diff --git a/homeassistant/components/nest/translations/ka.json b/homeassistant/components/nest/translations/ka.json index dfe103d5253..133efd8944f 100644 --- a/homeassistant/components/nest/translations/ka.json +++ b/homeassistant/components/nest/translations/ka.json @@ -1,7 +1,16 @@ { "config": { "abort": { - "no_url_available": "URL \u10db\u10d8\u10e3\u10ec\u10d5\u10d3\u10dd\u10db\u10d4\u10da\u10d8\u10d0. \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d0\u10e1 \u10d0\u10db \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 , [\u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 help \u10e1\u10d4\u10e5\u10ea\u10d8\u10d0] ({docs_url})" + "no_url_available": "URL \u10db\u10d8\u10e3\u10ec\u10d5\u10d3\u10dd\u10db\u10d4\u10da\u10d8\u10d0. \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d0\u10e1 \u10d0\u10db \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 , [\u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 help \u10e1\u10d4\u10e5\u10ea\u10d8\u10d0] ({docs_url})", + "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0." + }, + "create_entry": { + "default": "\u10d0\u10e7\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "step": { + "pick_implementation": { + "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 4708ddc5350..624ecb357ee 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index d584381bdcc..0ceab12cfc5 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/ovo_energy/translations/ka.json b/homeassistant/components/ovo_energy/translations/ka.json new file mode 100644 index 00000000000..7dd642123b2 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/ka.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + }, + "flow_title": "OVO Energy: {username}", + "step": { + "reauth": { + "data": { + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8" + }, + "description": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OVOEnergy-\u10e1\u10d7\u10d5\u10d8\u10e1. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7, \u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10dd\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4 \u10e1\u10d4\u10e0\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d4\u10d1\u10d8.", + "title": "\u10ee\u10d4\u10da\u10d0\u10ee\u10d0\u10da\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/cs.json b/homeassistant/components/point/translations/cs.json index 39670ea6ed5..6dedba8af11 100644 --- a/homeassistant/components/point/translations/cs.json +++ b/homeassistant/components/point/translations/cs.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "external_setup": "Point \u00fasp\u011b\u0161n\u011b nastaveno jin\u00fdm zp\u016fsobem.", - "no_flows": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + "no_flows": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 5374d2808d9..51c334e794f 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", - "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/point/translations/et.json b/homeassistant/components/point/translations/et.json index 7a26d227c8e..7317e2cd3e3 100644 --- a/homeassistant/components/point/translations/et.json +++ b/homeassistant/components/point/translations/et.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Tuvastamise URL'i loomise ajal\u00f5pp.", "external_setup": "Point on teisest voost edukalt seadistatud.", - "no_flows": "Osis pole seadistatud. Palun vaata dokumentatsiooni." + "no_flows": "Osis pole seadistatud. Palun vaata dokumentatsiooni.", + "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" diff --git a/homeassistant/components/point/translations/ka.json b/homeassistant/components/point/translations/ka.json new file mode 100644 index 00000000000..8e555221947 --- /dev/null +++ b/homeassistant/components/point/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index 2b4f431189e..59dff606f8f 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", "external_setup": "Punktet er konfigurert fra en annen flyt.", - "no_flows": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "no_flows": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index c4a58a064c4..bbab02f959e 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -5,7 +5,8 @@ "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\u3002", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", - "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/sharkiq/translations/ka.json b/homeassistant/components/sharkiq/translations/ka.json new file mode 100644 index 00000000000..44a03e20bec --- /dev/null +++ b/homeassistant/components/sharkiq/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ka.json b/homeassistant/components/simplisafe/translations/ka.json new file mode 100644 index 00000000000..30cd7df1991 --- /dev/null +++ b/homeassistant/components/simplisafe/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/cs.json b/homeassistant/components/solaredge/translations/cs.json index fab65e36174..e985ed4f221 100644 --- a/homeassistant/components/solaredge/translations/cs.json +++ b/homeassistant/components/solaredge/translations/cs.json @@ -8,7 +8,8 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "could_not_connect": "Nelze se p\u0159ipojit k API SolarEdge", "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", - "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven" + "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven", + "site_not_active": "Str\u00e1nka nen\u00ed aktivn\u00ed" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/ka.json b/homeassistant/components/solaredge/translations/ka.json new file mode 100644 index 00000000000..d6982775db2 --- /dev/null +++ b/homeassistant/components/solaredge/translations/ka.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "\u10db\u10dd\u10ec\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "error": { + "already_configured": "\u10db\u10dd\u10ec\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "could_not_connect": "Solaredge API- \u10e1\u10d7\u10d0\u10dc \u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", + "invalid_api_key": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 API key", + "site_not_active": "\u10e1\u10d0\u10d8\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d0\u10e5\u10e2\u10d8\u10e3\u10e0\u10d8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ka.json b/homeassistant/components/srp_energy/translations/ka.json index a281500e91c..f4e15ad5d9e 100644 --- a/homeassistant/components/srp_energy/translations/ka.json +++ b/homeassistant/components/srp_energy/translations/ka.json @@ -13,6 +13,7 @@ "user": { "data": { "id": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 ID", + "is_tou": "\u10d2\u10d4\u10d2\u10db\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10d3\u10e0\u10dd", "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8", "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8" } diff --git a/homeassistant/components/tellduslive/translations/cs.json b/homeassistant/components/tellduslive/translations/cs.json index 82355d34716..e0780ece8be 100644 --- a/homeassistant/components/tellduslive/translations/cs.json +++ b/homeassistant/components/tellduslive/translations/cs.json @@ -4,7 +4,8 @@ "already_configured": "Slu\u017eba je ji\u017e nastavena", "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 2e6f1a64419..7b39b7fe042 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", - "unknown": "Se produjo un error desconocido" + "unknown": "Se produjo un error desconocido", + "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/tellduslive/translations/et.json b/homeassistant/components/tellduslive/translations/et.json index b9008e7bc79..bab010bd845 100644 --- a/homeassistant/components/tellduslive/translations/et.json +++ b/homeassistant/components/tellduslive/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Teenus on juba seadistatud", "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.", - "unknown": "Tundmatu viga" + "unknown": "Tundmatu viga", + "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, "error": { "invalid_auth": "Tuvastamise viga" diff --git a/homeassistant/components/tellduslive/translations/ka.json b/homeassistant/components/tellduslive/translations/ka.json new file mode 100644 index 00000000000..8e555221947 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 13ad5419215..95bd22cbecf 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Tjenesten er allerede konfigurert", "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." }, "error": { "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/tellduslive/translations/zh-Hant.json b/homeassistant/components/tellduslive/translations/zh-Hant.json index e44c2c45416..4ce3d2c478e 100644 --- a/homeassistant/components/tellduslive/translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/toon/translations/cs.json b/homeassistant/components/toon/translations/cs.json index 52a2f7b5742..bf4de080873 100644 --- a/homeassistant/components/toon/translations/cs.json +++ b/homeassistant/components/toon/translations/cs.json @@ -4,7 +4,8 @@ "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", - "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index b6c6e7ad67d..6539388f76a 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/et.json b/homeassistant/components/toon/translations/et.json index a003702d654..7b70eae433e 100644 --- a/homeassistant/components/toon/translations/et.json +++ b/homeassistant/components/toon/translations/et.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Palun vaata dokumentatsiooni.", "no_agreements": "Sellel kontol ei ole Toon-i kuvasid.", - "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})" + "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})", + "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/ka.json b/homeassistant/components/toon/translations/ka.json new file mode 100644 index 00000000000..8e555221947 --- /dev/null +++ b/homeassistant/components/toon/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index 241f04c5b71..e5a72f35e2b 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "no_agreements": "Denne kontoen har ingen Toon skjermer.", - "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json index 020938792d6..daf5ff0ec18 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002", - "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "step": { "agreement": { diff --git a/homeassistant/components/water_heater/translations/ka.json b/homeassistant/components/water_heater/translations/ka.json new file mode 100644 index 00000000000..17ccabdb18e --- /dev/null +++ b/homeassistant/components/water_heater/translations/ka.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u10d2\u10d0\u10db\u10dd\u10e0\u10d7\u10d4\u10d3 {entity_name}", + "turn_on": "\u10e9\u10d0\u10e0\u10d7\u10d4\u10d3 {entity_name}" + } + } +} \ No newline at end of file From d4f9c1979f9e81de6413574714d13b079700d52a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 26 Nov 2020 11:38:30 +0100 Subject: [PATCH 242/430] Fix deadlock if an integration from stage_1 fails (#43657) --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index eff8a04ba92..b68fc9d17ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -528,7 +528,7 @@ async def _async_set_up_integrations( _LOGGER.warning("Setup timed out for stage 1 - moving forward") # Enables after dependencies - async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains) + async_set_domains_to_be_loaded(hass, stage_2_domains) if stage_2_domains: _LOGGER.info("Setting up stage 2: %s", stage_2_domains) From eb3e5cf446a486ffb560160735027994b3ee08fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Nov 2020 11:46:59 +0100 Subject: [PATCH 243/430] Suggest folder when importing blueprint and store source_url (#43650) --- homeassistant/components/blueprint/importer.py | 13 ++++++++----- homeassistant/components/blueprint/websocket_api.py | 1 - tests/components/blueprint/test_importer.py | 10 ++++++++-- tests/components/blueprint/test_websocket_api.py | 4 ++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 0f229ec8a07..f538c0897f0 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -41,7 +41,6 @@ COMMUNITY_TOPIC_SCHEMA = vol.Schema( class ImportedBlueprint: """Imported blueprint.""" - url: str suggested_filename: str raw_data: str blueprint: Blueprint @@ -125,7 +124,9 @@ def _extract_blueprint_from_community_topic( if blueprint is None: return None - return ImportedBlueprint(url, topic["slug"], block_content, blueprint) + return ImportedBlueprint( + f'{post["username"]}/{topic["slug"]}', block_content, blueprint + ) async def fetch_blueprint_from_community_post( @@ -159,18 +160,20 @@ async def fetch_blueprint_from_github_url( blueprint = Blueprint(data) parsed_import_url = yarl.URL(import_url) - suggested_filename = f"{parsed_import_url.parts[1]}-{parsed_import_url.parts[-1]}" + suggested_filename = f"{parsed_import_url.parts[1]}/{parsed_import_url.parts[-1]}" if suggested_filename.endswith(".yaml"): suggested_filename = suggested_filename[:-5] - return ImportedBlueprint(url, suggested_filename, raw_yaml, blueprint) + return ImportedBlueprint(suggested_filename, raw_yaml, blueprint) async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint: """Get a blueprint from a url.""" for func in (fetch_blueprint_from_community_post, fetch_blueprint_from_github_url): try: - return await func(hass, url) + imported_bp = await func(hass, url) + imported_bp.blueprint.update_metadata(source_url=url) + return imported_bp except ValueError: pass diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 1e6971d9bc8..6968d4530cd 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -79,7 +79,6 @@ async def ws_import_blueprint(hass, connection, msg): connection.send_result( msg["id"], { - "url": imported_blueprint.url, "suggested_filename": imported_blueprint.suggested_filename, "raw_data": imported_blueprint.raw_data, "blueprint": { diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 263f82ab230..3fa3dbab2f1 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -56,7 +56,6 @@ def test_extract_blueprint_from_community_topic(community_post): "http://example.com", json.loads(community_post) ) assert imported_blueprint is not None - assert imported_blueprint.url == "http://example.com" assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.placeholders == { "service_to_call", @@ -79,7 +78,7 @@ def test_extract_blueprint_from_community_topic_invalid_yaml(): ) -def test__extract_blueprint_from_community_topic_wrong_lang(): +def test_extract_blueprint_from_community_topic_wrong_lang(): """Test extracting blueprint with invalid YAML.""" assert ( importer._extract_blueprint_from_community_topic( @@ -110,6 +109,11 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit "service_to_call", "trigger_event", } + assert imported_blueprint.suggested_filename == "balloob/test-topic" + assert ( + imported_blueprint.blueprint.metadata["source_url"] + == "https://community.home-assistant.io/t/test-topic/123/2" + ) @pytest.mark.parametrize( @@ -135,3 +139,5 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): "service_to_call", "trigger_event", } + assert imported_blueprint.suggested_filename == "balloob/motion_light" + assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 32b7e7748e5..d79463ac3a6 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -85,14 +85,14 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["id"] == 5 assert msg["success"] assert msg["result"] == { - "suggested_filename": "balloob-motion_light", - "url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + "suggested_filename": "balloob/motion_light", "raw_data": raw_data, "blueprint": { "metadata": { "domain": "automation", "input": {"service_to_call": None, "trigger_event": None}, "name": "Call service based on event", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", }, }, "validation_errors": None, From 316a2750dfecda2df233911bb569f08fef4fdfc5 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Thu, 26 Nov 2020 15:42:55 +0100 Subject: [PATCH 244/430] Add Duty binary_sensor platform to FireServiceRota integration (#43638) --- .coveragerc | 1 + .../components/fireservicerota/__init__.py | 54 ++++++----- .../fireservicerota/binary_sensor.py | 93 +++++++++++++++++++ .../components/fireservicerota/const.py | 3 + .../components/fireservicerota/sensor.py | 22 ++--- 5 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/fireservicerota/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index bf6c81bf514..064fe422358 100644 --- a/.coveragerc +++ b/.coveragerc @@ -263,6 +263,7 @@ omit = homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py homeassistant/components/fireservicerota/__init__.py + homeassistant/components/fireservicerota/binary_sensor.py homeassistant/components/fireservicerota/const.py homeassistant/components/fireservicerota/sensor.py homeassistant/components/firmata/__init__.py diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index a0fc1d68d23..2eb29a95cf6 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -11,6 +11,7 @@ from pyfireservicerota import ( InvalidTokenError, ) +from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME @@ -18,13 +19,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, WSS_BWRURL +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN, WSS_BWRURL MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -SUPPORTED_PLATFORMS = {SENSOR_DOMAIN} +SUPPORTED_PLATFORMS = {SENSOR_DOMAIN, BINARYSENSOR_DOMAIN} async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -37,14 +38,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up FireServiceRota from a config entry.""" hass.data.setdefault(DOMAIN, {}) - coordinator = FireServiceRotaCoordinator(hass, entry) - await coordinator.setup() - await coordinator.async_availability_update() - if coordinator.token_refresh_failure: + client = FireServiceRotaClient(hass, entry) + await client.setup() + + if client.token_refresh_failure: return False - hass.data[DOMAIN][entry.entry_id] = coordinator + async def async_update_data(): + return await client.async_update() + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="duty binary sensor", + update_method=async_update_data, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + DATA_CLIENT: client, + DATA_COORDINATOR: coordinator, + } for platform in SUPPORTED_PLATFORMS: hass.async_create_task( @@ -161,7 +178,7 @@ class FireServiceRotaWebSocket: self._fsr_incidents.stop() -class FireServiceRotaCoordinator(DataUpdateCoordinator): +class FireServiceRotaClient: """Getting the latest data from fireservicerota.""" def __init__(self, hass, entry): @@ -169,14 +186,6 @@ class FireServiceRotaCoordinator(DataUpdateCoordinator): self._hass = hass self._entry = entry - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_method=self.async_availability_update, - update_interval=MIN_TIME_BETWEEN_UPDATES, - ) - self._url = entry.data[CONF_URL] self._tokens = entry.data[CONF_TOKEN] @@ -194,7 +203,7 @@ class FireServiceRotaCoordinator(DataUpdateCoordinator): self.websocket = FireServiceRotaWebSocket(self._hass, self._entry) async def setup(self) -> None: - """Set up the coordinator.""" + """Set up the data client.""" await self._hass.async_add_executor_job(self.websocket.start_listener) async def update_call(self, func, *args): @@ -207,23 +216,22 @@ class FireServiceRotaCoordinator(DataUpdateCoordinator): except (ExpiredTokenError, InvalidTokenError): self.websocket.stop_listener() self.token_refresh_failure = True - self.update_interval = None if await self.oauth.async_refresh_tokens(): - self.update_interval = MIN_TIME_BETWEEN_UPDATES self.token_refresh_failure = False self.websocket.start_listener() return await self._hass.async_add_executor_job(func, *args) - async def async_availability_update(self) -> None: + async def async_update(self) -> object: """Get the latest availability data.""" - _LOGGER.debug("Updating availability data") - - return await self.update_call( + data = await self.update_call( self.fsr.get_availability, str(self._hass.config.time_zone) ) + _LOGGER.debug("Updated availability data: %s", data) + return data + async def async_response_update(self) -> object: """Get the latest incident response data.""" data = self.websocket.incident_data() diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py new file mode 100644 index 00000000000..bef6ebe3f8d --- /dev/null +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -0,0 +1,93 @@ +"""Binary Sensor platform for FireServiceRota integration.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up FireServiceRota binary sensor based on a config entry.""" + + coordinator: DataUpdateCoordinator = hass.data[FIRESERVICEROTA_DOMAIN][ + entry.entry_id + ][DATA_COORDINATOR] + + async_add_entities([ResponseBinarySensor(coordinator, entry)]) + + +class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Representation of an FireServiceRota sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, entry): + """Initialize.""" + super().__init__(coordinator) + self._unique_id = f"{entry.unique_id}_Duty" + + self._state = None + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return "Duty" + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + return "mdi:calendar" + + @property + def unique_id(self) -> str: + """Return the unique ID for this binary sensor.""" + return self._unique_id + + @property + def is_on(self): + """Return the state of the binary sensor.""" + if not self.coordinator.data: + return + + data = self.coordinator.data + if "available" in data and data["available"]: + self._state = True + else: + self._state = False + + _LOGGER.debug("Set state of entity 'Duty Binary Sensor' to '%s'", self._state) + return self._state + + @property + def device_state_attributes(self): + """Return available attributes for binary sensor.""" + attr = {} + if not self.coordinator.data: + return attr + + data = self.coordinator.data + attr = { + key: data[key] + for key in ( + "start_time", + "end_time", + "available", + "active", + "assigned_function_ids", + "skill_ids", + "type", + "assigned_function", + ) + if key in data + } + + _LOGGER.debug("Set attributes of entity 'Duty Binary Sensor' to '%s'", attr) + return attr diff --git a/homeassistant/components/fireservicerota/const.py b/homeassistant/components/fireservicerota/const.py index 5ca0b7d7e64..9be0bfdc0ca 100644 --- a/homeassistant/components/fireservicerota/const.py +++ b/homeassistant/components/fireservicerota/const.py @@ -7,3 +7,6 @@ URL_LIST = { "www.fireservicerota.co.uk": "FireServiceRota", } WSS_BWRURL = "wss://{0}/cable?access_token={1}" + +DATA_CLIENT = "client" +DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 4360a834288..cba05f4fa5e 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -7,7 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN as FIRESERVICEROTA_DOMAIN +from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -16,19 +16,19 @@ async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota sensor based on a config entry.""" - coordinator = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id] + client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] - async_add_entities([IncidentsSensor(coordinator)]) + async_add_entities([IncidentsSensor(client)]) class IncidentsSensor(RestoreEntity): """Representation of FireServiceRota incidents sensor.""" - def __init__(self, coordinator): + def __init__(self, client): """Initialize.""" - self._coordinator = coordinator - self._entry_id = self._coordinator._entry.entry_id - self._unique_id = f"{self._coordinator._entry.unique_id}_Incidents" + self._client = client + self._entry_id = self._client._entry.entry_id + self._unique_id = f"{self._client._entry.unique_id}_Incidents" self._state = None self._state_attributes = {} @@ -112,14 +112,14 @@ class IncidentsSensor(RestoreEntity): async_dispatcher_connect( self.hass, f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update", - self.coordinator_update, + self.client_update, ) ) @callback - def coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - data = self._coordinator.websocket.incident_data() + def client_update(self) -> None: + """Handle updated data from the data client.""" + data = self._client.websocket.incident_data() if not data or "body" not in data: return From 4aa181416c8e000d9696bacdd9b79ab2c195e114 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 26 Nov 2020 15:44:25 +0100 Subject: [PATCH 245/430] Add area selector, remove date selector (#43658) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/selector.py | 26 +++++++++++++++---------- tests/helpers/test_selector.py | 32 +++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index d02a41f5f97..7ddd751753e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -24,7 +24,7 @@ def validate_selector(config: Any) -> Dict: if selector_class is None: raise vol.Invalid(f"Unknown selector type {selector_type} found") - # Seletors can be empty + # Selectors can be empty if config[selector_type] is None: return {selector_type: {}} @@ -67,10 +67,21 @@ class DeviceSelector(Selector): vol.Optional("manufacturer"): str, # Model of device vol.Optional("model"): str, + # Device has to contain entities matching this selector + vol.Optional( + "entity" + ): EntitySelector.CONFIG_SCHEMA, # pylint: disable=no-member } ) +@SELECTORS.register("area") +class AreaSelector(Selector): + """Selector of a single area.""" + + CONFIG_SCHEMA = vol.Schema({}) + + @SELECTORS.register("number") class NumberSelector(Selector): """Selector of a numeric value.""" @@ -95,13 +106,8 @@ class BooleanSelector(Selector): CONFIG_SCHEMA = vol.Schema({}) -@SELECTORS.register("datetime") -class DateTimeSelector(Selector): - """Selector of a date and or time value.""" +@SELECTORS.register("time") +class TimeSelector(Selector): + """Selector of a time value.""" - CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("has_date", default=False): bool, - vol.Optional("has_time", default=False): bool, - } - ) + CONFIG_SCHEMA = vol.Schema({}) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index c5b11285e48..9d2d57cd6d0 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -47,6 +47,13 @@ def test_validate_selector(): {"model": "mock-model"}, {"manufacturer": "mock-manuf", "model": "mock-model"}, {"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"}, + {"entity": {"device_class": "motion"}}, + { + "integration": "zha", + "manufacturer": "mock-manuf", + "model": "mock-model", + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + }, ), ) def test_device_selector_schema(schema): @@ -60,7 +67,9 @@ def test_device_selector_schema(schema): {}, {"integration": "zha"}, {"domain": "light"}, + {"device_class": "motion"}, {"integration": "zha", "domain": "light"}, + {"integration": "zha", "domain": "binary_sensor", "device_class": "motion"}, ), ) def test_entity_selector_schema(schema): @@ -68,6 +77,15 @@ def test_entity_selector_schema(schema): selector.validate_selector({"entity": schema}) +@pytest.mark.parametrize( + "schema", + ({},), +) +def test_area_selector_schema(schema): + """Test area selector.""" + selector.validate_selector({"area": schema}) + + @pytest.mark.parametrize( "schema", ( @@ -94,14 +112,8 @@ def test_boolean_selector_schema(schema): @pytest.mark.parametrize( "schema", - ( - {}, - {"has_date": True, "has_time": True}, - {"has_date": False, "has_time": False}, - {"has_date": True, "has_time": False}, - {"has_date": False, "has_time": True}, - ), + ({},), ) -def test_datetime_selector_schema(schema): - """Test datetime selector.""" - selector.validate_selector({"datetime": schema}) +def test_time_selector_schema(schema): + """Test time selector.""" + selector.validate_selector({"time": schema}) From 39efbcb815d3c8eb274ec06ef15c9f266e03dfba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Nov 2020 16:00:50 +0100 Subject: [PATCH 246/430] Allow importing gist (#43659) --- .../components/blueprint/importer.py | 64 ++++++++++++-- tests/components/blueprint/test_importer.py | 25 +++++- tests/fixtures/blueprint/github_gist.json | 84 +++++++++++++++++++ 3 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/blueprint/github_gist.json diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index f538c0897f0..bc40f76e7c2 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -25,7 +25,6 @@ COMMUNITY_CODE_BLOCK = re.compile( GITHUB_FILE_PATTERN = re.compile( r"^https://github.com/(?P.+)/blob/(?P.+)$" ) -GITHUB_RAW_FILE_PATTERN = re.compile(r"^https://raw.githubusercontent.com/") COMMUNITY_TOPIC_SCHEMA = vol.Schema( { @@ -37,6 +36,10 @@ COMMUNITY_TOPIC_SCHEMA = vol.Schema( ) +class UnsupportedUrl(HomeAssistantError): + """When the function doesn't support the url.""" + + @dataclass(frozen=True) class ImportedBlueprint: """Imported blueprint.""" @@ -51,14 +54,13 @@ def _get_github_import_url(url: str) -> str: Async friendly. """ - match = GITHUB_RAW_FILE_PATTERN.match(url) - if match is not None: + if url.startswith("https://raw.githubusercontent.com/"): return url match = GITHUB_FILE_PATTERN.match(url) if match is None: - raise ValueError("Not a GitHub file url") + raise UnsupportedUrl("Not a GitHub file url") repo, path = match.groups() @@ -72,7 +74,7 @@ def _get_community_post_import_url(url: str) -> str: """ match = COMMUNITY_TOPIC_PATTERN.match(url) if match is None: - raise ValueError("Not a topic url") + raise UnsupportedUrl("Not a topic url") _topic, post = match.groups() @@ -122,7 +124,7 @@ def _extract_blueprint_from_community_topic( break if blueprint is None: - return None + raise HomeAssistantError("No valid blueprint found in the topic") return ImportedBlueprint( f'{post["username"]}/{topic["slug"]}', block_content, blueprint @@ -167,14 +169,60 @@ async def fetch_blueprint_from_github_url( return ImportedBlueprint(suggested_filename, raw_yaml, blueprint) +async def fetch_blueprint_from_github_gist_url( + hass: HomeAssistant, url: str +) -> ImportedBlueprint: + """Get a blueprint from a Github Gist.""" + if not url.startswith("https://gist.github.com/"): + raise UnsupportedUrl("Not a GitHub gist url") + + parsed_url = yarl.URL(url) + session = aiohttp_client.async_get_clientsession(hass) + + resp = await session.get( + f"https://api.github.com/gists/{parsed_url.parts[2]}", + headers={"Accept": "application/vnd.github.v3+json"}, + raise_for_status=True, + ) + gist = await resp.json() + + blueprint = None + filename = None + content = None + + for filename, info in gist["files"].items(): + if not filename.endswith(".yaml"): + continue + + content = info["content"] + data = yaml.parse_yaml(content) + + if not is_blueprint_config(data): + continue + + blueprint = Blueprint(data) + break + + if blueprint is None: + raise HomeAssistantError("No valid blueprint found in the gist") + + return ImportedBlueprint( + f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint + ) + + async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint: """Get a blueprint from a url.""" - for func in (fetch_blueprint_from_community_post, fetch_blueprint_from_github_url): + for func in ( + fetch_blueprint_from_community_post, + fetch_blueprint_from_github_url, + fetch_blueprint_from_github_gist_url, + ): try: imported_bp = await func(hass, url) imported_bp.blueprint.update_metadata(source_url=url) return imported_bp - except ValueError: + except UnsupportedUrl: pass raise HomeAssistantError("Unsupported url") diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 3fa3dbab2f1..9d6bb48bcaf 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -80,8 +80,8 @@ def test_extract_blueprint_from_community_topic_invalid_yaml(): def test_extract_blueprint_from_community_topic_wrong_lang(): """Test extracting blueprint with invalid YAML.""" - assert ( - importer._extract_blueprint_from_community_topic( + with pytest.raises(importer.HomeAssistantError): + assert importer._extract_blueprint_from_community_topic( "http://example.com", { "post_stream": { @@ -91,8 +91,6 @@ def test_extract_blueprint_from_community_topic_wrong_lang(): } }, ) - is None - ) async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, community_post): @@ -141,3 +139,22 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url + + +async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock): + """Test fetching blueprint from url.""" + aioclient_mock.get( + "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344", + text=load_fixture("blueprint/github_gist.json"), + ) + + url = "https://gist.github.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344" + imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) + assert isinstance(imported_blueprint, importer.ImportedBlueprint) + assert imported_blueprint.blueprint.domain == "automation" + assert imported_blueprint.blueprint.placeholders == { + "motion_entity", + "light_entity", + } + assert imported_blueprint.suggested_filename == "balloob/motion_light" + assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/fixtures/blueprint/github_gist.json b/tests/fixtures/blueprint/github_gist.json new file mode 100644 index 00000000000..f25c7ca0238 --- /dev/null +++ b/tests/fixtures/blueprint/github_gist.json @@ -0,0 +1,84 @@ +{ + "url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344", + "forks_url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/forks", + "commits_url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/commits", + "id": "e717ce85dd0d2f1bdcdfc884ea25a344", + "node_id": "MDQ6R2lzdGU3MTdjZTg1ZGQwZDJmMWJkY2RmYzg4NGVhMjVhMzQ0", + "git_pull_url": "https://gist.github.com/e717ce85dd0d2f1bdcdfc884ea25a344.git", + "git_push_url": "https://gist.github.com/e717ce85dd0d2f1bdcdfc884ea25a344.git", + "html_url": "https://gist.github.com/e717ce85dd0d2f1bdcdfc884ea25a344", + "files": { + "motion_light.yaml": { + "filename": "motion_light.yaml", + "type": "text/x-yaml", + "language": "YAML", + "raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml", + "size": 803, + "truncated": false, + "content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !placeholder light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !placeholder light_entity\n" + } + }, + "public": false, + "created_at": "2020-11-25T22:49:50Z", + "updated_at": "2020-11-25T22:49:51Z", + "description": "Example gist", + "comments": 0, + "user": null, + "comments_url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/comments", + "owner": { + "login": "balloob", + "id": 1444314, + "node_id": "MDQ6VXNlcjE0NDQzMTQ=", + "avatar_url": "https://avatars3.githubusercontent.com/u/1444314?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/balloob", + "html_url": "https://github.com/balloob", + "followers_url": "https://api.github.com/users/balloob/followers", + "following_url": "https://api.github.com/users/balloob/following{/other_user}", + "gists_url": "https://api.github.com/users/balloob/gists{/gist_id}", + "starred_url": "https://api.github.com/users/balloob/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/balloob/subscriptions", + "organizations_url": "https://api.github.com/users/balloob/orgs", + "repos_url": "https://api.github.com/users/balloob/repos", + "events_url": "https://api.github.com/users/balloob/events{/privacy}", + "received_events_url": "https://api.github.com/users/balloob/received_events", + "type": "User", + "site_admin": false + }, + "forks": [ + + ], + "history": [ + { + "user": { + "login": "balloob", + "id": 1444314, + "node_id": "MDQ6VXNlcjE0NDQzMTQ=", + "avatar_url": "https://avatars3.githubusercontent.com/u/1444314?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/balloob", + "html_url": "https://github.com/balloob", + "followers_url": "https://api.github.com/users/balloob/followers", + "following_url": "https://api.github.com/users/balloob/following{/other_user}", + "gists_url": "https://api.github.com/users/balloob/gists{/gist_id}", + "starred_url": "https://api.github.com/users/balloob/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/balloob/subscriptions", + "organizations_url": "https://api.github.com/users/balloob/orgs", + "repos_url": "https://api.github.com/users/balloob/repos", + "events_url": "https://api.github.com/users/balloob/events{/privacy}", + "received_events_url": "https://api.github.com/users/balloob/received_events", + "type": "User", + "site_admin": false + }, + "version": "0b1028d04209ad0cc7942d3dcf02f9b036cea21f", + "committed_at": "2020-11-25T22:49:50Z", + "change_status": { + "total": 38, + "additions": 38, + "deletions": 0 + }, + "url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/0b1028d04209ad0cc7942d3dcf02f9b036cea21f" + } + ], + "truncated": false +} From dc8364fd3a33f302356c3a0f7248b0ab9160088d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Nov 2020 16:45:02 +0100 Subject: [PATCH 247/430] Support disabling devices (#43293) --- .../components/config/device_registry.py | 3 + homeassistant/helpers/device_registry.py | 28 +++++++++ homeassistant/helpers/entity_registry.py | 38 +++++++++--- .../components/config/test_device_registry.py | 4 ++ tests/helpers/test_device_registry.py | 5 ++ tests/helpers/test_entity_registry.py | 61 ++++++++++++++++++- 6 files changed, 128 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index de1f38f3e57..a43a863444a 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -21,6 +21,8 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( vol.Required("device_id"): str, vol.Optional("area_id"): vol.Any(str, None), vol.Optional("name_by_user"): vol.Any(str, None), + # We only allow setting disabled_by user via API. + vol.Optional("disabled_by"): vol.Any("user", None), } ) @@ -77,4 +79,5 @@ def _entry_dict(entry): "via_device_id": entry.via_device_id, "area_id": entry.area_id, "name_by_user": entry.name_by_user, + "disabled_by": entry.disabled_by, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 388db62ebae..cc8f9a17827 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -37,6 +37,9 @@ IDX_IDENTIFIERS = "identifiers" REGISTERED_DEVICE = "registered" DELETED_DEVICE = "deleted" +DISABLED_INTEGRATION = "integration" +DISABLED_USER = "user" + @attr.s(slots=True, frozen=True) class DeletedDeviceEntry: @@ -76,6 +79,21 @@ class DeviceEntry: id: str = attr.ib(factory=uuid_util.random_uuid_hex) # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) + disabled_by: Optional[str] = attr.ib( + default=None, + validator=attr.validators.in_( + ( + DISABLED_INTEGRATION, + DISABLED_USER, + None, + ) + ), + ) + + @property + def disabled(self) -> bool: + """Return if entry is disabled.""" + return self.disabled_by is not None def format_mac(mac: str) -> str: @@ -215,6 +233,8 @@ class DeviceRegistry: sw_version=_UNDEF, entry_type=_UNDEF, via_device=None, + # To disable a device if it gets created + disabled_by=_UNDEF, ): """Get device. Create if it doesn't exist.""" if not identifiers and not connections: @@ -267,6 +287,7 @@ class DeviceRegistry: name=name, sw_version=sw_version, entry_type=entry_type, + disabled_by=disabled_by, ) @callback @@ -283,6 +304,7 @@ class DeviceRegistry: sw_version=_UNDEF, via_device_id=_UNDEF, remove_config_entry_id=_UNDEF, + disabled_by=_UNDEF, ): """Update properties of a device.""" return self._async_update_device( @@ -296,6 +318,7 @@ class DeviceRegistry: sw_version=sw_version, via_device_id=via_device_id, remove_config_entry_id=remove_config_entry_id, + disabled_by=disabled_by, ) @callback @@ -316,6 +339,7 @@ class DeviceRegistry: via_device_id=_UNDEF, area_id=_UNDEF, name_by_user=_UNDEF, + disabled_by=_UNDEF, ): """Update device attributes.""" old = self.devices[device_id] @@ -362,6 +386,7 @@ class DeviceRegistry: ("sw_version", sw_version), ("entry_type", entry_type), ("via_device_id", via_device_id), + ("disabled_by", disabled_by), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -440,6 +465,8 @@ class DeviceRegistry: # Introduced in 0.87 area_id=device.get("area_id"), name_by_user=device.get("name_by_user"), + # Introduced in 0.119 + disabled_by=device.get("disabled_by"), ) # Introduced in 0.111 for device in data.get("deleted_devices", []): @@ -478,6 +505,7 @@ class DeviceRegistry: "via_device_id": entry.via_device_id, "area_id": entry.area_id, "name_by_user": entry.name_by_user, + "disabled_by": entry.disabled_by, } for entry in self.devices.values() ] diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 872d87e732f..143f3a99137 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -53,9 +53,10 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) _UNDEF = object() DISABLED_CONFIG_ENTRY = "config_entry" +DISABLED_DEVICE = "device" DISABLED_HASS = "hass" -DISABLED_USER = "user" DISABLED_INTEGRATION = "integration" +DISABLED_USER = "user" STORAGE_VERSION = 1 STORAGE_KEY = "core.entity_registry" @@ -89,10 +90,11 @@ class RegistryEntry: default=None, validator=attr.validators.in_( ( - DISABLED_HASS, - DISABLED_USER, - DISABLED_INTEGRATION, DISABLED_CONFIG_ENTRY, + DISABLED_DEVICE, + DISABLED_HASS, + DISABLED_INTEGRATION, + DISABLED_USER, None, ) ), @@ -127,7 +129,7 @@ class EntityRegistry: self._index: Dict[Tuple[str, str, str], str] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self.hass.bus.async_listen( - EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_removed + EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified ) @callback @@ -286,18 +288,34 @@ class EntityRegistry: ) self.async_schedule_save() - @callback - def async_device_removed(self, event: Event) -> None: - """Handle the removal of a device. + async def async_device_modified(self, event: Event) -> None: + """Handle the removal or update of a device. Remove entities from the registry that are associated to a device when the device is removed. + + Disable entities in the registry that are associated to a device when + the device is disabled. """ - if event.data["action"] != "remove": + if event.data["action"] == "remove": + entities = async_entries_for_device(self, event.data["device_id"]) + for entity in entities: + self.async_remove(entity.entity_id) return + + if event.data["action"] != "update": + return + + device_registry = await self.hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(event.data["device_id"]) + if not device.disabled: + return + entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: - self.async_remove(entity.entity_id) + self.async_update_entity( # type: ignore + entity.entity_id, disabled_by=DISABLED_DEVICE + ) @callback def async_update_entity( diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 1e1cbccf60a..b2273d640de 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -56,6 +56,7 @@ async def test_list_devices(hass, client, registry): "via_device_id": None, "area_id": None, "name_by_user": None, + "disabled_by": None, }, { "config_entries": ["1234"], @@ -69,6 +70,7 @@ async def test_list_devices(hass, client, registry): "via_device_id": dev1, "area_id": None, "name_by_user": None, + "disabled_by": None, }, ] @@ -92,6 +94,7 @@ async def test_update_device(hass, client, registry): "device_id": device.id, "area_id": "12345A", "name_by_user": "Test Friendly Name", + "disabled_by": "user", "type": "config/device_registry/update", } ) @@ -101,4 +104,5 @@ async def test_update_device(hass, client, registry): assert msg["result"]["id"] == device.id assert msg["result"]["area_id"] == "12345A" assert msg["result"]["name_by_user"] == "Test Friendly Name" + assert msg["result"]["disabled_by"] == "user" assert len(registry.devices) == 1 diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 85ff693f261..7fa787e023e 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -152,6 +152,7 @@ async def test_loading_from_storage(hass, hass_storage): "entry_type": "service", "area_id": "12345A", "name_by_user": "Test Friendly Name", + "disabled_by": "user", } ], "deleted_devices": [ @@ -180,6 +181,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" assert entry.entry_type == "service" + assert entry.disabled_by == "user" assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -445,6 +447,7 @@ async def test_loading_saving_data(hass, registry): manufacturer="manufacturer", model="light", via_device=("hue", "0123"), + disabled_by="user", ) orig_light2 = registry.async_get_or_create( @@ -581,6 +584,7 @@ async def test_update(registry): name_by_user="Test Friendly Name", new_identifiers=new_identifiers, via_device_id="98765B", + disabled_by="user", ) assert mock_save.call_count == 1 @@ -591,6 +595,7 @@ async def test_update(registry): assert updated_entry.name_by_user == "Test Friendly Name" assert updated_entry.identifiers == new_identifiers assert updated_entry.via_device_id == "98765B" + assert updated_entry.disabled_by == "user" assert registry.async_get_device({("hue", "456")}, {}) is None assert registry.async_get_device({("bla", "123")}, {}) is None diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 336329396cc..f42661ec915 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -9,7 +9,12 @@ from homeassistant.helpers import entity_registry import tests.async_mock from tests.async_mock import patch -from tests.common import MockConfigEntry, flush_store, mock_registry +from tests.common import ( + MockConfigEntry, + flush_store, + mock_device_registry, + mock_registry, +) YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -677,3 +682,57 @@ async def test_async_get_device_class_lookup(hass): ("sensor", "battery"): "sensor.vacuum_battery", }, } + + +async def test_remove_device_removes_entities(hass, registry): + """Test that we remove entities tied to a device.""" + device_registry = mock_device_registry(hass) + config_entry = MockConfigEntry(domain="light") + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + + entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + assert registry.async_is_registered(entry.entity_id) + + device_registry.async_remove_device(device_entry.id) + await hass.async_block_till_done() + + assert not registry.async_is_registered(entry.entity_id) + + +async def test_disable_device_disables_entities(hass, registry): + """Test that we remove entities tied to a device.""" + device_registry = mock_device_registry(hass) + config_entry = MockConfigEntry(domain="light") + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + + entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + assert not entry.disabled + + device_registry.async_update_device(device_entry.id, disabled_by="user") + await hass.async_block_till_done() + + entry = registry.async_get(entry.entity_id) + assert entry.disabled + assert entry.disabled_by == "device" From f3033ec01d127ccb78a2abe834fc3e3cc89fd89f Mon Sep 17 00:00:00 2001 From: Jasper Slits Date: Thu, 26 Nov 2020 17:48:54 +0100 Subject: [PATCH 248/430] Add Roomba support for automatic emptying of bin (#43594) --- homeassistant/components/roomba/__init__.py | 2 +- homeassistant/components/roomba/config_flow.py | 2 +- homeassistant/components/roomba/irobot_base.py | 1 + homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roomba/test_config_flow.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 912b134d454..be85ec3619f 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -3,7 +3,7 @@ import asyncio import logging import async_timeout -from roomba import Roomba, RoombaConnectionError +from roombapy import Roomba, RoombaConnectionError import voluptuous as vol from homeassistant import config_entries, exceptions diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index b25c4ece440..166b5992d86 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure roomba component.""" -from roomba import Roomba +from roombapy import Roomba import voluptuous as vol from homeassistant import config_entries, core diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 8bc1e22547f..7dd045a1137 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -51,6 +51,7 @@ SUPPORT_IROBOT = ( STATE_MAP = { "": STATE_IDLE, "charge": STATE_DOCKED, + "evac": STATE_RETURNING, # Emptying at cleanbase "hmMidMsn": STATE_CLEANING, # Recharging at the middle of a cycle "hmPostMsn": STATE_RETURNING, # Cycle finished "hmUsrDock": STATE_RETURNING, diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 9b0e3f21983..808c7eb9432 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,6 +3,6 @@ "name": "iRobot Roomba", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.1"], + "requirements": ["roombapy==1.6.2"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"] } diff --git a/requirements_all.txt b/requirements_all.txt index 30904d7520e..2ef308552a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1957,7 +1957,7 @@ rocketchat-API==0.6.1 rokuecp==0.6.0 # homeassistant.components.roomba -roombapy==1.6.1 +roombapy==1.6.2 # homeassistant.components.roon roonapi==0.0.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 862157e45ce..10cbffe6e84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ ring_doorbell==0.6.0 rokuecp==0.6.0 # homeassistant.components.roomba -roombapy==1.6.1 +roombapy==1.6.2 # homeassistant.components.roon roonapi==0.0.25 diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index d2af07070bb..253250d7d49 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1,5 +1,5 @@ """Test the iRobot Roomba config flow.""" -from roomba import RoombaConnectionError +from roombapy import RoombaConnectionError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.roomba.const import ( From edf70e9f06639123ff4221f1ab56b775e3303cd8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Nov 2020 20:20:10 +0100 Subject: [PATCH 249/430] Make input_datetime better handle timezones (#43396) * Make input_datetime better handle timezones * Improve test coverage * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Revert change to time format Co-authored-by: Martin Hjelmare --- .../components/input_datetime/__init__.py | 170 ++++++++++-------- .../input_datetime/reproduce_state.py | 40 +---- homeassistant/core.py | 16 +- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/service.py | 4 +- homeassistant/util/dt.py | 3 + tests/components/input_datetime/test_init.py | 128 +++++++++---- .../input_datetime/test_reproduce_state.py | 6 + 8 files changed, 227 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index e95287d2cbe..aa1f0b8814a 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,5 +1,5 @@ """Support to select a date and/or a time.""" -import datetime +import datetime as py_datetime import logging import typing @@ -33,12 +33,16 @@ CONF_HAS_TIME = "has_time" CONF_INITIAL = "initial" DEFAULT_VALUE = "1970-01-01 00:00:00" -DEFAULT_DATE = datetime.date(1970, 1, 1) -DEFAULT_TIME = datetime.time(0, 0, 0) +DEFAULT_DATE = py_datetime.date(1970, 1, 1) +DEFAULT_TIME = py_datetime.time(0, 0, 0) ATTR_DATETIME = "datetime" ATTR_TIMESTAMP = "timestamp" +FMT_DATE = "%Y-%m-%d" +FMT_TIME = "%H:%M:%S" +FMT_DATETIME = f"{FMT_DATE} {FMT_TIME}" + def validate_set_datetime_attrs(config): """Validate set_datetime service attributes.""" @@ -51,20 +55,6 @@ def validate_set_datetime_attrs(config): return config -SERVICE_SET_DATETIME = "set_datetime" -SERVICE_SET_DATETIME_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_DATETIME): cv.datetime, - vol.Optional(ATTR_TIMESTAMP): vol.Coerce(float), - }, - extra=vol.ALLOW_EXTRA, - ), - cv.has_at_least_one_key(ATTR_DATE, ATTR_TIME, ATTR_DATETIME, ATTR_TIMESTAMP), - validate_set_datetime_attrs, -) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -162,31 +152,24 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=RELOAD_SERVICE_SCHEMA, ) - async def async_set_datetime_service(entity, call): - """Handle a call to the input datetime 'set datetime' service.""" - date = call.data.get(ATTR_DATE) - time = call.data.get(ATTR_TIME) - dttm = call.data.get(ATTR_DATETIME) - tmsp = call.data.get(ATTR_TIMESTAMP) - - if tmsp: - dttm = dt_util.as_local(dt_util.utc_from_timestamp(tmsp)).replace( - tzinfo=None - ) - if dttm: - date = dttm.date() - time = dttm.time() - if not entity.has_date: - date = None - if not entity.has_time: - time = None - if not date and not time: - raise vol.Invalid("Nothing to set") - - entity.async_set_datetime(date, time) - component.async_register_entity_service( - SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service + "set_datetime", + vol.All( + vol.Schema( + { + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_DATETIME): cv.datetime, + vol.Optional(ATTR_TIMESTAMP): vol.Coerce(float), + }, + extra=vol.ALLOW_EXTRA, + ), + cv.has_at_least_one_key( + ATTR_DATE, ATTR_TIME, ATTR_DATETIME, ATTR_TIMESTAMP + ), + validate_set_datetime_attrs, + ), + "async_set_datetime", ) return True @@ -221,16 +204,31 @@ class InputDatetime(RestoreEntity): self._config = config self.editable = True self._current_datetime = None + initial = config.get(CONF_INITIAL) - if initial: - if self.has_date and self.has_time: - self._current_datetime = dt_util.parse_datetime(initial) - elif self.has_date: - date = dt_util.parse_date(initial) - self._current_datetime = datetime.datetime.combine(date, DEFAULT_TIME) - else: - time = dt_util.parse_time(initial) - self._current_datetime = datetime.datetime.combine(DEFAULT_DATE, time) + if not initial: + return + + if self.has_date and self.has_time: + current_datetime = dt_util.parse_datetime(initial) + + elif self.has_date: + date = dt_util.parse_date(initial) + current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) + + else: + time = dt_util.parse_time(initial) + current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time) + + # If the user passed in an initial value with a timezone, convert it to right tz + if current_datetime.tzinfo is not None: + self._current_datetime = current_datetime.astimezone( + dt_util.DEFAULT_TIME_ZONE + ) + else: + self._current_datetime = dt_util.DEFAULT_TIME_ZONE.localize( + current_datetime + ) @classmethod def from_yaml(cls, config: typing.Dict) -> "InputDatetime": @@ -257,21 +255,27 @@ class InputDatetime(RestoreEntity): if self.has_date and self.has_time: date_time = dt_util.parse_datetime(old_state.state) if date_time is None: - self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) - return - self._current_datetime = date_time + current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + else: + current_datetime = date_time + elif self.has_date: date = dt_util.parse_date(old_state.state) if date is None: - self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) - return - self._current_datetime = datetime.datetime.combine(date, DEFAULT_TIME) + current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + else: + current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) + else: time = dt_util.parse_time(old_state.state) if time is None: - self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) - return - self._current_datetime = datetime.datetime.combine(DEFAULT_DATE, time) + current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + else: + current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time) + + self._current_datetime = current_datetime.replace( + tzinfo=dt_util.DEFAULT_TIME_ZONE + ) @property def should_poll(self): @@ -305,10 +309,12 @@ class InputDatetime(RestoreEntity): return None if self.has_date and self.has_time: - return self._current_datetime + return self._current_datetime.strftime(FMT_DATETIME) + if self.has_date: - return self._current_datetime.date() - return self._current_datetime.time() + return self._current_datetime.strftime(FMT_DATE) + + return self._current_datetime.strftime(FMT_TIME) @property def state_attributes(self): @@ -338,11 +344,13 @@ class InputDatetime(RestoreEntity): + self._current_datetime.minute * 60 + self._current_datetime.second ) + elif not self.has_time: - extended = datetime.datetime.combine( - self._current_datetime, datetime.time(0, 0) + extended = py_datetime.datetime.combine( + self._current_datetime, py_datetime.time(0, 0) ) attrs["timestamp"] = extended.timestamp() + else: attrs["timestamp"] = self._current_datetime.timestamp() @@ -354,13 +362,35 @@ class InputDatetime(RestoreEntity): return self._config[CONF_ID] @callback - def async_set_datetime(self, date_val, time_val): + def async_set_datetime(self, date=None, time=None, datetime=None, timestamp=None): """Set a new date / time.""" - if not date_val: - date_val = self._current_datetime.date() - if not time_val: - time_val = self._current_datetime.time() - self._current_datetime = datetime.datetime.combine(date_val, time_val) + if timestamp: + datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp)).replace( + tzinfo=None + ) + + if datetime: + date = datetime.date() + time = datetime.time() + + if not self.has_date: + date = None + + if not self.has_time: + time = None + + if not date and not time: + raise vol.Invalid("Nothing to set") + + if not date: + date = self._current_datetime.date() + + if not time: + time = self._current_datetime.time() + + self._current_datetime = py_datetime.datetime.combine(date, time).replace( + tzinfo=dt_util.DEFAULT_TIME_ZONE + ) self.async_write_ha_state() async def async_update_config(self, config: typing.Dict) -> None: diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index b9eb9800adf..cc906ac50b3 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -8,15 +8,7 @@ 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, -) +from . import ATTR_DATE, ATTR_DATETIME, ATTR_TIME, CONF_HAS_DATE, CONF_HAS_TIME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -53,22 +45,13 @@ async def _async_reproduce_state( _LOGGER.warning("Unable to find entity %s", state.entity_id) return + has_time = cur_state.attributes.get(CONF_HAS_TIME) + has_date = cur_state.attributes.get(CONF_HAS_DATE) + 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) - ) + (is_valid_datetime(state.state) and has_date and has_time) + or (is_valid_date(state.state) and has_date and not has_time) + or (is_valid_time(state.state) and has_time and not has_date) ): _LOGGER.warning( "Invalid state specified for %s: %s", state.entity_id, state.state @@ -79,24 +62,17 @@ async def _async_reproduce_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 + DOMAIN, "set_datetime", service_data, context=context, blocking=True ) diff --git a/homeassistant/core.py b/homeassistant/core.py index 68f0b9a30b7..9eeaf6fccca 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -419,7 +419,9 @@ class HomeAssistant: self._track_task = False @callback - def async_run_hass_job(self, hassjob: HassJob, *args: Any) -> None: + def async_run_hass_job( + self, hassjob: HassJob, *args: Any + ) -> Optional[asyncio.Future]: """Run a HassJob from within the event loop. This method must be run in the event loop. @@ -429,13 +431,14 @@ class HomeAssistant: """ if hassjob.job_type == HassJobType.Callback: hassjob.target(*args) - else: - self.async_add_hass_job(hassjob, *args) + return None + + return self.async_add_hass_job(hassjob, *args) @callback def async_run_job( self, target: Callable[..., Union[None, Awaitable]], *args: Any - ) -> None: + ) -> Optional[asyncio.Future]: """Run a job from within the event loop. This method must be run in the event loop. @@ -444,10 +447,9 @@ class HomeAssistant: args: parameters for method to call. """ if asyncio.iscoroutine(target): - self.async_create_task(cast(Coroutine, target)) - return + return self.async_create_task(cast(Coroutine, target)) - self.async_run_hass_job(HassJob(target), *args) + return self.async_run_hass_job(HassJob(target), *args) def block_till_done(self) -> None: """Block until all pending work is done.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index b9d073dbe2e..0f1f04e3aec 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -192,7 +192,7 @@ class EntityComponent: self, name: str, schema: Union[Dict[str, Any], vol.Schema], - func: str, + func: Union[str, Callable[..., Any]], required_features: Optional[List[int]] = None, ) -> None: """Register an entity service.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 06d0ae46ae3..6a290a77b08 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -527,9 +527,9 @@ async def _handle_entity_call( entity.async_set_context(context) if isinstance(func, str): - result = hass.async_add_job(partial(getattr(entity, func), **data)) # type: ignore + result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore else: - result = hass.async_add_job(func, entity, data) + result = hass.async_run_job(func, entity, data) # Guard because callback functions do not return a task when passed to async_add_job. if result is not None: diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 8b52170f0e4..a4d5fd81c4f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,6 +108,9 @@ def start_of_local_day( date: dt.date = now().date() elif isinstance(dt_or_d, dt.datetime): date = dt_or_d.date() + else: + date = dt_or_d + return DEFAULT_TIME_ZONE.localize( # type: ignore dt.datetime.combine(date, dt.time()) ) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 70f0b69d3ef..53914a83ba2 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -16,10 +16,13 @@ from homeassistant.components.input_datetime import ( CONF_ID, CONF_INITIAL, CONF_NAME, + CONFIG_SCHEMA, DEFAULT_TIME, DOMAIN, + FMT_DATE, + FMT_DATETIME, + FMT_TIME, SERVICE_RELOAD, - SERVICE_SET_DATETIME, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME from homeassistant.core import Context, CoreState, State @@ -35,6 +38,8 @@ INITIAL_DATE = "2020-01-10" INITIAL_TIME = "23:45:56" INITIAL_DATETIME = f"{INITIAL_DATE} {INITIAL_TIME}" +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + @pytest.fixture def storage_setup(hass, hass_storage): @@ -74,7 +79,7 @@ async def async_set_date_and_time(hass, entity_id, dt_value): """Set date and / or time of input_datetime.""" await hass.services.async_call( DOMAIN, - SERVICE_SET_DATETIME, + "set_datetime", { ATTR_ENTITY_ID: entity_id, ATTR_DATE: dt_value.date(), @@ -88,7 +93,7 @@ async def async_set_datetime(hass, entity_id, dt_value): """Set date and / or time of input_datetime.""" await hass.services.async_call( DOMAIN, - SERVICE_SET_DATETIME, + "set_datetime", {ATTR_ENTITY_ID: entity_id, ATTR_DATETIME: dt_value}, blocking=True, ) @@ -98,22 +103,24 @@ async def async_set_timestamp(hass, entity_id, timestamp): """Set date and / or time of input_datetime.""" await hass.services.async_call( DOMAIN, - SERVICE_SET_DATETIME, + "set_datetime", {ATTR_ENTITY_ID: entity_id, ATTR_TIMESTAMP: timestamp}, blocking=True, ) -async def test_invalid_configs(hass): - """Test config.""" - invalid_configs = [ +@pytest.mark.parametrize( + "config", + [ None, - {}, {"name with space": None}, {"test_no_value": {"has_time": False, "has_date": False}}, - ] - for cfg in invalid_configs: - assert not await async_setup_component(hass, DOMAIN, {DOMAIN: cfg}) + ], +) +def test_invalid_configs(config): + """Test config.""" + with pytest.raises(vol.Invalid): + CONFIG_SCHEMA({DOMAIN: config}) async def test_set_datetime(hass): @@ -129,7 +136,7 @@ async def test_set_datetime(hass): await async_set_date_and_time(hass, entity_id, dt_obj) state = hass.states.get(entity_id) - assert state.state == str(dt_obj) + assert state.state == dt_obj.strftime(FMT_DATETIME) assert state.attributes["has_time"] assert state.attributes["has_date"] @@ -155,7 +162,7 @@ async def test_set_datetime_2(hass): await async_set_datetime(hass, entity_id, dt_obj) state = hass.states.get(entity_id) - assert state.state == str(dt_obj) + assert state.state == dt_obj.strftime(FMT_DATETIME) assert state.attributes["has_time"] assert state.attributes["has_date"] @@ -181,7 +188,7 @@ async def test_set_datetime_3(hass): await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) state = hass.states.get(entity_id) - assert state.state == str(dt_obj) + assert state.state == dt_obj.strftime(FMT_DATETIME) assert state.attributes["has_time"] assert state.attributes["has_date"] @@ -203,12 +210,11 @@ async def test_set_datetime_time(hass): entity_id = "input_datetime.test_time" dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) - time_portion = dt_obj.time() await async_set_date_and_time(hass, entity_id, dt_obj) state = hass.states.get(entity_id) - assert state.state == str(time_portion) + assert state.state == dt_obj.strftime(FMT_TIME) assert state.attributes["has_time"] assert not state.attributes["has_date"] @@ -240,7 +246,6 @@ async def test_set_invalid(hass): {"entity_id": entity_id, "time": time_portion}, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == initial @@ -329,7 +334,7 @@ async def test_restore_state(hass): "test_bogus_data": { "has_time": True, "has_date": True, - "initial": str(initial), + "initial": initial.strftime(FMT_DATETIME), }, "test_was_time": {"has_time": False, "has_date": True}, "test_was_date": {"has_time": True, "has_date": False}, @@ -339,22 +344,22 @@ async def test_restore_state(hass): dt_obj = datetime.datetime(2017, 9, 7, 19, 46) state_time = hass.states.get("input_datetime.test_time") - assert state_time.state == str(dt_obj.time()) + assert state_time.state == dt_obj.strftime(FMT_TIME) state_date = hass.states.get("input_datetime.test_date") - assert state_date.state == str(dt_obj.date()) + assert state_date.state == dt_obj.strftime(FMT_DATE) state_datetime = hass.states.get("input_datetime.test_datetime") - assert state_datetime.state == str(dt_obj) + assert state_datetime.state == dt_obj.strftime(FMT_DATETIME) state_bogus = hass.states.get("input_datetime.test_bogus_data") - assert state_bogus.state == str(initial) + assert state_bogus.state == initial.strftime(FMT_DATETIME) state_was_time = hass.states.get("input_datetime.test_was_time") - assert state_was_time.state == str(default.date()) + assert state_was_time.state == default.strftime(FMT_DATE) state_was_date = hass.states.get("input_datetime.test_was_date") - assert state_was_date.state == str(default.time()) + assert state_was_date.state == default.strftime(FMT_TIME) async def test_default_value(hass): @@ -373,15 +378,15 @@ async def test_default_value(hass): dt_obj = datetime.datetime(1970, 1, 1, 0, 0) state_time = hass.states.get("input_datetime.test_time") - assert state_time.state == str(dt_obj.time()) + assert state_time.state == dt_obj.strftime(FMT_TIME) assert state_time.attributes.get("timestamp") is not None state_date = hass.states.get("input_datetime.test_date") - assert state_date.state == str(dt_obj.date()) + assert state_date.state == dt_obj.strftime(FMT_DATE) assert state_date.attributes.get("timestamp") is not None state_datetime = hass.states.get("input_datetime.test_datetime") - assert state_datetime.state == str(dt_obj) + assert state_datetime.state == dt_obj.strftime(FMT_DATETIME) assert state_datetime.attributes.get("timestamp") is not None @@ -434,7 +439,7 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_1 is not None assert state_2 is None assert state_3 is not None - assert str(dt_obj.date()) == state_1.state + assert dt_obj.strftime(FMT_DATE) == state_1.state assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") == f"{DOMAIN}.dt3" @@ -473,8 +478,8 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_1 is not None assert state_2 is not None assert state_3 is None - assert str(DEFAULT_TIME) == state_1.state - assert str(datetime.datetime(1970, 1, 1, 0, 0)) == state_2.state + assert state_1.state == DEFAULT_TIME.strftime(FMT_TIME) + assert state_2.state == datetime.datetime(1970, 1, 1, 0, 0).strftime(FMT_DATETIME) assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") == f"{DOMAIN}.dt2" @@ -641,3 +646,66 @@ async def test_setup_no_config(hass, hass_admin_user): await hass.async_block_till_done() assert count_start == len(hass.states.async_entity_ids()) + + +async def test_timestamp(hass): + """Test timestamp.""" + try: + dt_util.set_default_time_zone(dt_util.get_time_zone("America/Los_Angeles")) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_datetime_initial_with_tz": { + "has_time": True, + "has_date": True, + "initial": "2020-12-13 10:00:00+01:00", + }, + "test_datetime_initial_without_tz": { + "has_time": True, + "has_date": True, + "initial": "2020-12-13 10:00:00", + }, + "test_time_initial": { + "has_time": True, + "has_date": False, + "initial": "10:00:00", + }, + } + }, + ) + + # initial has been converted to the set timezone + state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz") + assert state_with_tz is not None + assert state_with_tz.state == "2020-12-13 01:00:00" + assert ( + dt_util.as_local( + dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP]) + ).strftime(FMT_DATETIME) + == "2020-12-13 01:00:00" + ) + + # initial has been interpreted as being part of set timezone + state_without_tz = hass.states.get( + "input_datetime.test_datetime_initial_without_tz" + ) + assert state_without_tz is not None + assert state_without_tz.state == "2020-12-13 10:00:00" + assert ( + dt_util.as_local( + dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) + + # Test initial time sets timestamp correctly. + state_time = hass.states.get("input_datetime.test_time_initial") + assert state_time is not None + assert state_time.state == "10:00:00" + assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 + + finally: + dt_util.set_default_time_zone(ORIG_TIMEZONE) diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index 87d737dfb4f..f2d9dd4d445 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -19,6 +19,11 @@ async def test_reproducing_states(hass, caplog): "2010-10-10", {"has_date": True, "has_time": False}, ) + hass.states.async_set( + "input_datetime.invalid_data", + "unavailable", + {"has_date": False, "has_time": False}, + ) datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") @@ -57,6 +62,7 @@ async def test_reproducing_states(hass, caplog): State("input_datetime.entity_date", "2011-10-10"), # Should not raise State("input_datetime.non_existing", "2010-10-10 01:20:00"), + State("input_datetime.invalid_data", "2010-10-10 01:20:00"), ], ) From e1de36fda80aeb57e3246a1bd573d363672c63d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Nov 2020 22:25:21 +0100 Subject: [PATCH 250/430] Fix check config (#43663) --- homeassistant/components/blueprint/models.py | 24 +++++++++-- homeassistant/helpers/check_config.py | 28 +++++++++---- tests/components/blueprint/test_models.py | 12 +++++- tests/helpers/test_check_config.py | 42 ++++++++++++++++---- 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 85c87b5baa7..6e79b6da842 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -212,7 +212,13 @@ class DomainBlueprints: """Load a blueprint.""" try: blueprint_data = yaml.load_yaml(self.blueprint_folder / blueprint_path) - except (HomeAssistantError, FileNotFoundError) as err: + except FileNotFoundError as err: + raise FailedToLoad( + self.domain, + blueprint_path, + FileNotFoundError(f"Unable to find {blueprint_path}"), + ) from err + except HomeAssistantError as err: raise FailedToLoad(self.domain, blueprint_path, err) from err return Blueprint( @@ -251,13 +257,25 @@ class DomainBlueprints: async def async_get_blueprint(self, blueprint_path: str) -> Blueprint: """Get a blueprint.""" + + def load_from_cache(): + """Load blueprint from cache.""" + blueprint = self._blueprints[blueprint_path] + if blueprint is None: + raise FailedToLoad( + self.domain, + blueprint_path, + FileNotFoundError(f"Unable to find {blueprint_path}"), + ) + return blueprint + if blueprint_path in self._blueprints: - return self._blueprints[blueprint_path] + return load_from_cache() async with self._load_lock: # Check it again if blueprint_path in self._blueprints: - return self._blueprints[blueprint_path] + return load_from_cache() try: blueprint = await self.hass.async_add_executor_job( diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 018bbe5cfe0..c98b563ac7e 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,9 +1,9 @@ """Helper to check the configuration file.""" from collections import OrderedDict +import logging import os from typing import List, NamedTuple, Optional -import attr import voluptuous as vol from homeassistant import loader @@ -36,11 +36,13 @@ class CheckConfigError(NamedTuple): config: Optional[ConfigType] -@attr.s class HomeAssistantConfig(OrderedDict): """Configuration result with errors attribute.""" - errors: List[CheckConfigError] = attr.ib(factory=list) + def __init__(self) -> None: + """Initialize HA config.""" + super().__init__() + self.errors: List[CheckConfigError] = [] def add_error( self, @@ -139,14 +141,24 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig config_validator, "async_validate_config" ): try: - return await config_validator.async_validate_config( # type: ignore - hass, config - ) + result[domain] = ( + await config_validator.async_validate_config( # type: ignore + hass, config + ) + )[domain] + continue except (vol.Invalid, HomeAssistantError) as ex: _comp_error(ex, domain, config) continue - except Exception: # pylint: disable=broad-except - result.add_error("Unknown error calling %s config validator", domain) + except Exception as err: # pylint: disable=broad-except + logging.getLogger(__name__).exception( + "Unexpected error validating config" + ) + result.add_error( + f"Unexpected error calling config validator: {err}", + domain, + config.get(domain), + ) continue config_schema = getattr(component, "CONFIG_SCHEMA", None) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 5c2d5f965ff..f5d94a9301a 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -203,8 +203,8 @@ async def test_domain_blueprints_get_blueprint_errors(hass, domain_bps): with patch( "homeassistant.util.yaml.load_yaml", return_value={"blueprint": "invalid"} - ): - assert await domain_bps.async_get_blueprint("non-existing-path") is None + ), pytest.raises(errors.FailedToLoad): + await domain_bps.async_get_blueprint("non-existing-path") async def test_domain_blueprints_caching(domain_bps): @@ -258,3 +258,11 @@ async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): with patch.object(domain_bps, "_load_blueprint") as mock_load: assert await domain_bps.async_get_blueprint("something.yaml") == blueprint_1 assert not mock_load.mock_calls + + +async def test_inputs_from_config_nonexisting_blueprint(domain_bps): + """Test referring non-existing blueprint.""" + with pytest.raises(errors.FailedToLoad): + await domain_bps.async_inputs_from_config( + {"use_blueprint": {"path": "non-existing.yaml"}} + ) diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 1c25dbedcde..13ce52a840f 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -7,8 +7,8 @@ from homeassistant.helpers.check_config import ( async_check_ha_config_file, ) -from tests.async_mock import patch -from tests.common import patch_yaml_files +from tests.async_mock import Mock, patch +from tests.common import mock_platform, patch_yaml_files _LOGGER = logging.getLogger(__name__) @@ -37,7 +37,7 @@ def log_ha_config(conf): _LOGGER.debug("error[%s] = %s", cnt, err) -async def test_bad_core_config(hass, loop): +async def test_bad_core_config(hass): """Test a bad core config setup.""" files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): @@ -53,7 +53,7 @@ async def test_bad_core_config(hass, loop): assert not res.errors -async def test_config_platform_valid(hass, loop): +async def test_config_platform_valid(hass): """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): @@ -65,7 +65,7 @@ async def test_config_platform_valid(hass, loop): assert not res.errors -async def test_component_platform_not_found(hass, loop): +async def test_component_platform_not_found(hass): """Test errors if component or platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} @@ -83,7 +83,7 @@ async def test_component_platform_not_found(hass, loop): assert not res.errors -async def test_component_platform_not_found_2(hass, loop): +async def test_component_platform_not_found_2(hass): """Test errors if component or platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"} @@ -103,7 +103,7 @@ async def test_component_platform_not_found_2(hass, loop): assert not res.errors -async def test_package_invalid(hass, loop): +async def test_package_invalid(hass): """Test a valid platform setup.""" files = { YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') @@ -121,7 +121,7 @@ async def test_package_invalid(hass, loop): assert res.keys() == {"homeassistant"} -async def test_bootstrap_error(hass, loop): +async def test_bootstrap_error(hass): """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "automation: !include no.yaml"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): @@ -146,6 +146,7 @@ automation: input: trigger_event: blueprint_event service_to_call: test.automation +input_datetime: """, hass.config.path( "blueprints/automation/test_event_service.yaml" @@ -166,3 +167,28 @@ action: with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) assert len(res.get("automation", [])) == 1 + assert len(res.errors) == 0 + assert "input_datetime" in res + + +async def test_config_platform_raise(hass): + """Test bad config validation platform.""" + mock_platform( + hass, + "bla.config", + Mock(async_validate_config=Mock(side_effect=Exception("Broken"))), + ) + files = { + YAML_CONFIG_FILE: BASE_CONFIG + + """ +bla: + value: 1 +""", + } + with patch("os.path.isfile", return_value=True), patch_yaml_files(files): + res = await async_check_ha_config_file(hass) + assert len(res.errors) == 1 + err = res.errors[0] + assert err.domain == "bla" + assert err.message == "Unexpected error calling config validator: Broken" + assert err.config == {"value": 1} From 8ff10ad3b803f48fc405e5fafe61f973540d20f8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Nov 2020 22:33:58 +0100 Subject: [PATCH 251/430] Fix MQTT threading bug (#43667) --- homeassistant/components/mqtt/__init__.py | 2 +- tests/components/mqtt/test_init.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 5898b1918a2..27b43142f7c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -944,7 +944,7 @@ class MQTT: ) birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) - self.hass.loop.create_task(publish_birth_message(birth_message)) + self.hass.add_job(publish_birth_message(birth_message)) def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: """Message received callback.""" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 39ea8bc92ec..86b905f2b0f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -638,8 +638,9 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m assert mqtt_client_mock.subscribe.call_count == 1 mqtt_mock._mqtt_on_disconnect(None, None, 0) - mqtt_mock._mqtt_on_connect(None, None, None, 0) - await hass.async_block_till_done() + with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + mqtt_mock._mqtt_on_connect(None, None, None, 0) + await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 2 @@ -671,8 +672,9 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.unsubscribe.call_count == 0 mqtt_mock._mqtt_on_disconnect(None, None, 0) - mqtt_mock._mqtt_on_connect(None, None, None, 0) - await hass.async_block_till_done() + with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + mqtt_mock._mqtt_on_connect(None, None, None, 0) + await hass.async_block_till_done() expected.append(call("test/state", 1)) assert mqtt_client_mock.subscribe.mock_calls == expected From deb45f7c9015b9ef631249759d4536fac2931cf2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 26 Nov 2020 22:34:17 +0100 Subject: [PATCH 252/430] Upgrade pre-commit to 2.9.2 (#43655) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 77c68763894..8ec5a611f1d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ coverage==5.3 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.790 -pre-commit==2.8.2 +pre-commit==2.9.2 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 From 974e099e2a9527d38445531c6d9bc1461ba4c36f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Nov 2020 22:34:26 +0100 Subject: [PATCH 253/430] Small cleanup of Tasmota (#43642) --- .../components/tasmota/binary_sensor.py | 5 +- homeassistant/components/tasmota/cover.py | 5 +- .../components/tasmota/device_automation.py | 2 +- homeassistant/components/tasmota/fan.py | 5 +- homeassistant/components/tasmota/light.py | 5 +- homeassistant/components/tasmota/mixins.py | 5 +- homeassistant/components/tasmota/sensor.py | 213 ++++++------------ homeassistant/components/tasmota/switch.py | 5 +- 8 files changed, 84 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/tasmota/binary_sensor.py b/homeassistant/components/tasmota/binary_sensor.py index ade309840ca..feaafa72b29 100644 --- a/homeassistant/components/tasmota/binary_sensor.py +++ b/homeassistant/components/tasmota/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt -from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DATA_REMOVE_DISCOVER_COMPONENT.format(binary_sensor.DOMAIN) ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(binary_sensor.DOMAIN, TASMOTA_DOMAIN), + TASMOTA_DISCOVERY_ENTITY_NEW.format(binary_sensor.DOMAIN), async_discover, ) @@ -47,7 +47,6 @@ class TasmotaBinarySensor( self._state = None super().__init__( - discovery_update=self.discovery_update, **kwds, ) diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py index 3556f6ebf52..681778d0099 100644 --- a/homeassistant/components/tasmota/cover.py +++ b/homeassistant/components/tasmota/cover.py @@ -7,7 +7,7 @@ from homeassistant.components.cover import CoverEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DATA_REMOVE_DISCOVER_COMPONENT.format(cover.DOMAIN) ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(cover.DOMAIN, TASMOTA_DOMAIN), + TASMOTA_DISCOVERY_ENTITY_NEW.format(cover.DOMAIN), async_discover, ) @@ -44,7 +44,6 @@ class TasmotaCover( self._position = None super().__init__( - discovery_update=self.discovery_update, **kwds, ) diff --git a/homeassistant/components/tasmota/device_automation.py b/homeassistant/components/tasmota/device_automation.py index aab0064bb96..ff431141bef 100644 --- a/homeassistant/components/tasmota/device_automation.py +++ b/homeassistant/components/tasmota/device_automation.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry): DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation") ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation", "tasmota"), + TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation"), async_discover, ) hass.data[DATA_UNSUB].append( diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 34aca5fb0ae..362149e9fca 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -7,7 +7,7 @@ from homeassistant.components.fan import FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DATA_REMOVE_DISCOVER_COMPONENT.format(fan.DOMAIN) ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(fan.DOMAIN, TASMOTA_DOMAIN), + TASMOTA_DISCOVERY_ENTITY_NEW.format(fan.DOMAIN), async_discover, ) @@ -52,7 +52,6 @@ class TasmotaFan( self._state = None super().__init__( - discovery_update=self.discovery_update, **kwds, ) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index a680d873f9f..c2e145600f8 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -27,7 +27,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate @@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DATA_REMOVE_DISCOVER_COMPONENT.format(light.DOMAIN) ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN, TASMOTA_DOMAIN), + TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN), async_discover, ) @@ -74,7 +74,6 @@ class TasmotaLight( self._flash_times = None super().__init__( - discovery_update=self.discovery_update, **kwds, ) diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index a860b06c574..d8e0eeeb4cd 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -116,10 +116,9 @@ class TasmotaAvailability(TasmotaEntity): class TasmotaDiscoveryUpdate(TasmotaEntity): """Mixin used to handle updated discovery message.""" - def __init__(self, discovery_hash, discovery_update, **kwds) -> None: + def __init__(self, discovery_hash, **kwds) -> None: """Initialize the discovery update mixin.""" self._discovery_hash = discovery_hash - self._discovery_update = discovery_update self._removed_from_hass = False super().__init__(**kwds) @@ -138,7 +137,7 @@ class TasmotaDiscoveryUpdate(TasmotaEntity): if not self._tasmota_entity.config_same(config): # Changed payload: Notify component _LOGGER.debug("Updating component: %s", self.entity_id) - await self._discovery_update(config) + await self.discovery_update(config) else: # Unchanged payload: Ignore to avoid changing states _LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 966bf8648d4..17a6e2a35c2 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -1,77 +1,7 @@ """Support for Tasmota sensors.""" from typing import Optional -from hatasmota import status_sensor -from hatasmota.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER as TASMOTA_CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION as TASMOTA_CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_MILLION as TASMOTA_CONCENTRATION_PARTS_PER_MILLION, - ELECTRICAL_CURRENT_AMPERE as TASMOTA_ELECTRICAL_CURRENT_AMPERE, - ELECTRICAL_VOLT_AMPERE as TASMOTA_ELECTRICAL_VOLT_AMPERE, - ENERGY_KILO_WATT_HOUR as TASMOTA_ENERGY_KILO_WATT_HOUR, - FREQUENCY_HERTZ as TASMOTA_FREQUENCY_HERTZ, - LENGTH_CENTIMETERS as TASMOTA_LENGTH_CENTIMETERS, - LIGHT_LUX as TASMOTA_LIGHT_LUX, - MASS_KILOGRAMS as TASMOTA_MASS_KILOGRAMS, - PERCENTAGE as TASMOTA_PERCENTAGE, - POWER_WATT as TASMOTA_POWER_WATT, - PRESSURE_HPA as TASMOTA_PRESSURE_HPA, - SENSOR_AMBIENT, - SENSOR_APPARENT_POWERUSAGE, - SENSOR_BATTERY, - SENSOR_CCT, - SENSOR_CO2, - SENSOR_COLOR_BLUE, - SENSOR_COLOR_GREEN, - SENSOR_COLOR_RED, - SENSOR_CURRENT, - SENSOR_DEWPOINT, - SENSOR_DISTANCE, - SENSOR_ECO2, - SENSOR_FREQUENCY, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_MOISTURE, - SENSOR_PB0_3, - SENSOR_PB0_5, - SENSOR_PB1, - SENSOR_PB2_5, - SENSOR_PB5, - SENSOR_PB10, - SENSOR_PM1, - SENSOR_PM2_5, - SENSOR_PM10, - SENSOR_POWERFACTOR, - SENSOR_POWERUSAGE, - SENSOR_PRESSURE, - SENSOR_PRESSUREATSEALEVEL, - SENSOR_PROXIMITY, - SENSOR_REACTIVE_POWERUSAGE, - SENSOR_STATUS_IP, - SENSOR_STATUS_LAST_RESTART_TIME, - SENSOR_STATUS_LINK_COUNT, - SENSOR_STATUS_MQTT_COUNT, - SENSOR_STATUS_RESTART_REASON, - SENSOR_STATUS_RSSI, - SENSOR_STATUS_SIGNAL, - SENSOR_STATUS_SSID, - SENSOR_TEMPERATURE, - SENSOR_TODAY, - SENSOR_TOTAL, - SENSOR_TOTAL_START_TIME, - SENSOR_TVOC, - SENSOR_VOLTAGE, - SENSOR_WEIGHT, - SENSOR_YESTERDAY, - SIGNAL_STRENGTH_DECIBELS as TASMOTA_SIGNAL_STRENGTH_DECIBELS, - SPEED_KILOMETERS_PER_HOUR as TASMOTA_SPEED_KILOMETERS_PER_HOUR, - SPEED_METERS_PER_SECOND as TASMOTA_SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR as TASMOTA_SPEED_MILES_PER_HOUR, - TEMP_CELSIUS as TASMOTA_TEMP_CELSIUS, - TEMP_FAHRENHEIT as TASMOTA_TEMP_FAHRENHEIT, - TEMP_KELVIN as TASMOTA_TEMP_KELVIN, - VOLT as TASMOTA_VOLT, -) +from hatasmota import const as hc, status_sensor from homeassistant.components import sensor from homeassistant.const import ( @@ -109,7 +39,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate @@ -118,77 +48,77 @@ ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both SENSOR_DEVICE_CLASS_ICON_MAP = { - SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, - SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY}, - SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, - SENSOR_CO2: {ICON: "mdi:molecule-co2"}, - SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, - SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, - SENSOR_COLOR_RED: {ICON: "mdi:palette"}, - SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"}, - SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"}, - SENSOR_DISTANCE: {ICON: "mdi:leak"}, - SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, - SENSOR_FREQUENCY: {ICON: "mdi:current-ac"}, - SENSOR_HUMIDITY: {DEVICE_CLASS: DEVICE_CLASS_HUMIDITY}, - SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, - SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, - SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, - SENSOR_MOISTURE: {ICON: "mdi:cup-water"}, - SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"}, - SENSOR_PB0_3: {ICON: "mdi:flask"}, - SENSOR_PB0_5: {ICON: "mdi:flask"}, - SENSOR_PB10: {ICON: "mdi:flask"}, - SENSOR_PB1: {ICON: "mdi:flask"}, - SENSOR_PB2_5: {ICON: "mdi:flask"}, - SENSOR_PB5: {ICON: "mdi:flask"}, - SENSOR_PM10: {ICON: "mdi:air-filter"}, - SENSOR_PM1: {ICON: "mdi:air-filter"}, - SENSOR_PM2_5: {ICON: "mdi:air-filter"}, - SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"}, - SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - SENSOR_PRESSURE: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE}, - SENSOR_PRESSUREATSEALEVEL: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE}, - SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, - SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"}, - SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, - SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, - SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, - SENSOR_TEMPERATURE: {DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, - SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, - SENSOR_TVOC: {ICON: "mdi:air-filter"}, - SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"}, - SENSOR_WEIGHT: {ICON: "mdi:scale"}, - SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, + hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY}, + hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, + hc.SENSOR_CO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, + hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, + hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"}, + hc.SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"}, + hc.SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"}, + hc.SENSOR_DISTANCE: {ICON: "mdi:leak"}, + hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_FREQUENCY: {ICON: "mdi:current-ac"}, + hc.SENSOR_HUMIDITY: {DEVICE_CLASS: DEVICE_CLASS_HUMIDITY}, + hc.SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, + hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, + hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, + hc.SENSOR_MOISTURE: {ICON: "mdi:cup-water"}, + hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"}, + hc.SENSOR_PB0_3: {ICON: "mdi:flask"}, + hc.SENSOR_PB0_5: {ICON: "mdi:flask"}, + hc.SENSOR_PB10: {ICON: "mdi:flask"}, + hc.SENSOR_PB1: {ICON: "mdi:flask"}, + hc.SENSOR_PB2_5: {ICON: "mdi:flask"}, + hc.SENSOR_PB5: {ICON: "mdi:flask"}, + hc.SENSOR_PM10: {ICON: "mdi:air-filter"}, + hc.SENSOR_PM1: {ICON: "mdi:air-filter"}, + hc.SENSOR_PM2_5: {ICON: "mdi:air-filter"}, + hc.SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"}, + hc.SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_PRESSURE: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE}, + hc.SENSOR_PRESSUREATSEALEVEL: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE}, + hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, + hc.SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + hc.SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"}, + hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, + hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, + hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, + hc.SENSOR_TEMPERATURE: {DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, + hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, + hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, + hc.SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"}, + hc.SENSOR_WEIGHT: {ICON: "mdi:scale"}, + hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, } SENSOR_UNIT_MAP = { - TASMOTA_CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TASMOTA_CONCENTRATION_PARTS_PER_BILLION: CONCENTRATION_PARTS_PER_BILLION, - TASMOTA_CONCENTRATION_PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION, - TASMOTA_ELECTRICAL_CURRENT_AMPERE: ELECTRICAL_CURRENT_AMPERE, - TASMOTA_ELECTRICAL_VOLT_AMPERE: ELECTRICAL_VOLT_AMPERE, - TASMOTA_ENERGY_KILO_WATT_HOUR: ENERGY_KILO_WATT_HOUR, - TASMOTA_FREQUENCY_HERTZ: FREQUENCY_HERTZ, - TASMOTA_LENGTH_CENTIMETERS: LENGTH_CENTIMETERS, - TASMOTA_LIGHT_LUX: LIGHT_LUX, - TASMOTA_MASS_KILOGRAMS: MASS_KILOGRAMS, - TASMOTA_PERCENTAGE: PERCENTAGE, - TASMOTA_POWER_WATT: POWER_WATT, - TASMOTA_PRESSURE_HPA: PRESSURE_HPA, - TASMOTA_SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, - TASMOTA_SPEED_KILOMETERS_PER_HOUR: SPEED_KILOMETERS_PER_HOUR, - TASMOTA_SPEED_METERS_PER_SECOND: SPEED_METERS_PER_SECOND, - TASMOTA_SPEED_MILES_PER_HOUR: SPEED_MILES_PER_HOUR, - TASMOTA_TEMP_CELSIUS: TEMP_CELSIUS, - TASMOTA_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT, - TASMOTA_TEMP_KELVIN: TEMP_KELVIN, - TASMOTA_VOLT: VOLT, + hc.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + hc.CONCENTRATION_PARTS_PER_BILLION: CONCENTRATION_PARTS_PER_BILLION, + hc.CONCENTRATION_PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION, + hc.ELECTRICAL_CURRENT_AMPERE: ELECTRICAL_CURRENT_AMPERE, + hc.ELECTRICAL_VOLT_AMPERE: ELECTRICAL_VOLT_AMPERE, + hc.ENERGY_KILO_WATT_HOUR: ENERGY_KILO_WATT_HOUR, + hc.FREQUENCY_HERTZ: FREQUENCY_HERTZ, + hc.LENGTH_CENTIMETERS: LENGTH_CENTIMETERS, + hc.LIGHT_LUX: LIGHT_LUX, + hc.MASS_KILOGRAMS: MASS_KILOGRAMS, + hc.PERCENTAGE: PERCENTAGE, + hc.POWER_WATT: POWER_WATT, + hc.PRESSURE_HPA: PRESSURE_HPA, + hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, + hc.SPEED_KILOMETERS_PER_HOUR: SPEED_KILOMETERS_PER_HOUR, + hc.SPEED_METERS_PER_SECOND: SPEED_METERS_PER_SECOND, + hc.SPEED_MILES_PER_HOUR: SPEED_MILES_PER_HOUR, + hc.TEMP_CELSIUS: TEMP_CELSIUS, + hc.TEMP_FAHRENHEIT: TEMP_FAHRENHEIT, + hc.TEMP_KELVIN: TEMP_KELVIN, + hc.VOLT: VOLT, } @@ -209,7 +139,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DATA_REMOVE_DISCOVER_COMPONENT.format(sensor.DOMAIN) ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN, TASMOTA_DOMAIN), + TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN), async_discover_sensor, ) @@ -222,7 +152,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity): self._state = None super().__init__( - discovery_update=self.discovery_update, **kwds, ) diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py index 0a97a5e2528..27906bf5dbb 100644 --- a/homeassistant/components/tasmota/switch.py +++ b/homeassistant/components/tasmota/switch.py @@ -5,7 +5,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN +from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): DATA_REMOVE_DISCOVER_COMPONENT.format(switch.DOMAIN) ] = async_dispatcher_connect( hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN, TASMOTA_DOMAIN), + TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN), async_discover, ) @@ -45,7 +45,6 @@ class TasmotaSwitch( self._state = False super().__init__( - discovery_update=self.discovery_update, **kwds, ) From 301a3e54816d2518110f646470a6fb0b61618e4e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 27 Nov 2020 00:03:57 +0000 Subject: [PATCH 254/430] [ci skip] Translation update --- .../advantage_air/translations/ka.json | 17 +++++++++++ .../components/agent_dvr/translations/ka.json | 7 +++++ .../components/airvisual/translations/ka.json | 15 ++++++++++ .../components/atag/translations/ka.json | 7 +++++ .../components/daikin/translations/ka.json | 16 ++++++++++ .../components/esphome/translations/ka.json | 7 +++++ .../fireservicerota/translations/cs.json | 29 +++++++++++++++++++ .../fireservicerota/translations/et.json | 29 +++++++++++++++++++ .../fireservicerota/translations/it.json | 29 +++++++++++++++++++ .../fireservicerota/translations/ka.json | 29 +++++++++++++++++++ .../fireservicerota/translations/no.json | 29 +++++++++++++++++++ .../fireservicerota/translations/tr.json | 19 ++++++++++++ .../fireservicerota/translations/zh-Hant.json | 29 +++++++++++++++++++ .../components/fritzbox/translations/ka.json | 10 +++++++ .../components/gree/translations/ka.json | 13 +++++++++ .../components/heos/translations/ka.json | 10 +++++++ .../components/homekit/translations/ka.json | 19 ++++++++++++ .../components/iaqualink/translations/ka.json | 7 +++++ .../components/icloud/translations/ka.json | 16 ++++++++++ .../components/ipp/translations/ka.json | 10 +++++++ .../components/nest/translations/it.json | 3 +- .../ovo_energy/translations/ka.json | 1 + .../components/plugwise/translations/ka.json | 12 +++++++- .../components/point/translations/it.json | 3 +- .../ruckus_unleashed/translations/ka.json | 21 ++++++++++++++ .../components/tasmota/translations/ka.json | 9 ++++++ .../tellduslive/translations/it.json | 3 +- .../tellduslive/translations/ka.json | 3 ++ .../components/tesla/translations/ka.json | 7 +++++ .../components/tibber/translations/ka.json | 7 +++++ .../components/toon/translations/it.json | 3 +- .../components/upcloud/translations/ka.json | 25 ++++++++++++++++ .../components/withings/translations/ka.json | 7 +++++ .../components/xbox/translations/ka.json | 17 +++++++++++ .../xiaomi_miio/translations/ka.json | 7 +++++ 35 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/advantage_air/translations/ka.json create mode 100644 homeassistant/components/agent_dvr/translations/ka.json create mode 100644 homeassistant/components/airvisual/translations/ka.json create mode 100644 homeassistant/components/atag/translations/ka.json create mode 100644 homeassistant/components/daikin/translations/ka.json create mode 100644 homeassistant/components/esphome/translations/ka.json create mode 100644 homeassistant/components/fireservicerota/translations/cs.json create mode 100644 homeassistant/components/fireservicerota/translations/et.json create mode 100644 homeassistant/components/fireservicerota/translations/it.json create mode 100644 homeassistant/components/fireservicerota/translations/ka.json create mode 100644 homeassistant/components/fireservicerota/translations/no.json create mode 100644 homeassistant/components/fireservicerota/translations/tr.json create mode 100644 homeassistant/components/fireservicerota/translations/zh-Hant.json create mode 100644 homeassistant/components/fritzbox/translations/ka.json create mode 100644 homeassistant/components/gree/translations/ka.json create mode 100644 homeassistant/components/heos/translations/ka.json create mode 100644 homeassistant/components/homekit/translations/ka.json create mode 100644 homeassistant/components/iaqualink/translations/ka.json create mode 100644 homeassistant/components/icloud/translations/ka.json create mode 100644 homeassistant/components/ipp/translations/ka.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/ka.json create mode 100644 homeassistant/components/tasmota/translations/ka.json create mode 100644 homeassistant/components/tesla/translations/ka.json create mode 100644 homeassistant/components/tibber/translations/ka.json create mode 100644 homeassistant/components/upcloud/translations/ka.json create mode 100644 homeassistant/components/withings/translations/ka.json create mode 100644 homeassistant/components/xbox/translations/ka.json create mode 100644 homeassistant/components/xiaomi_miio/translations/ka.json diff --git a/homeassistant/components/advantage_air/translations/ka.json b/homeassistant/components/advantage_air/translations/ka.json new file mode 100644 index 00000000000..4216ece47d2 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/ka.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8", + "port": "\u10de\u10dd\u10e0\u10e2\u10d8" + }, + "description": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Advantage Air API-\u10e1 \u10d9\u10d4\u10d3\u10d4\u10da\u10d6\u10d4 \u10d3\u10d0\u10db\u10dd\u10dc\u10e2\u10d0\u10df\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d0\u10d1\u10da\u10d4\u10e2\u10d8\u10d7", + "title": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ka.json b/homeassistant/components/agent_dvr/translations/ka.json new file mode 100644 index 00000000000..fa4c9c0abd3 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ka.json b/homeassistant/components/airvisual/translations/ka.json new file mode 100644 index 00000000000..cb01b5d0d14 --- /dev/null +++ b/homeassistant/components/airvisual/translations/ka.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8 \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "title": "AirVisual \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/ka.json b/homeassistant/components/atag/translations/ka.json new file mode 100644 index 00000000000..fa4c9c0abd3 --- /dev/null +++ b/homeassistant/components/atag/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/ka.json b/homeassistant/components/daikin/translations/ka.json new file mode 100644 index 00000000000..e777a22e8ce --- /dev/null +++ b/homeassistant/components/daikin/translations/ka.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1", + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", + "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ka.json b/homeassistant/components/esphome/translations/ka.json new file mode 100644 index 00000000000..d0a8e7c5666 --- /dev/null +++ b/homeassistant/components/esphome/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/cs.json b/homeassistant/components/fireservicerota/translations/cs.json new file mode 100644 index 00000000000..7ae758dab52 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "reauth": { + "data": { + "password": "Heslo" + }, + "description": "Ov\u011b\u0159ovac\u00ed tokeny jsou neplatn\u00e9. Chcete-li je znovu vytvo\u0159it, p\u0159ihlaste se." + }, + "user": { + "data": { + "password": "Heslo", + "url": "Webov\u00e1 str\u00e1nka", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/et.json b/homeassistant/components/fireservicerota/translations/et.json new file mode 100644 index 00000000000..27fc2a9b4d7 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taasautentimine \u00f5nnestus" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "error": { + "invalid_auth": "Vigane autentimine" + }, + "step": { + "reauth": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Tuvastusstring aegus, taasloomiseks logi sisse." + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "url": "Veebisait", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json new file mode 100644 index 00000000000..8fc43f294ec --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La riautenticazione ha avuto successo" + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "I token di autenticazione non sono validi, effettua il login per ricrearli." + }, + "user": { + "data": { + "password": "Password", + "url": "Sito web", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ka.json b/homeassistant/components/fireservicerota/translations/ka.json new file mode 100644 index 00000000000..422f3137d5c --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/ka.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "reauth_successful": "\u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10ea\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10d8\u10d7" + }, + "create_entry": { + "default": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10ea\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10d8\u10d7" + }, + "error": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + }, + "step": { + "reauth": { + "data": { + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8" + }, + "description": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e2\u10dd\u10d9\u10d4\u10dc\u10d8 \u10db\u10ea\u10d3\u10d0\u10e0\u10d8\u10d0, \u10e8\u10d4\u10d3\u10d8\u10d7 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d0\u10e8\u10d8 \u10ee\u10d4\u10da\u10d0\u10ee\u10da\u10d0 \u10e8\u10d4\u10e1\u10d0\u10e5\u10db\u10dc\u10d4\u10da\u10d0\u10d3." + }, + "user": { + "data": { + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8", + "url": "\u10d5\u10d4\u10d1\u10e1\u10d0\u10d8\u10e2\u10d8", + "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10d4\u10da\u10d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json new file mode 100644 index 00000000000..5a4635e1ed8 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Reautentisering var vellykket" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "reauth": { + "data": { + "password": "Passord" + }, + "description": "Autentiseringstokener for baceame er ugyldige, logg inn for \u00e5 gjenskape dem." + }, + "user": { + "data": { + "password": "Passord", + "url": "Nettsted", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json new file mode 100644 index 00000000000..a2d2cab3b74 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "\u015eifre" + }, + "description": "Kimlik do\u011frulama jetonlar\u0131 ge\u00e7ersiz, yeniden olu\u015fturmak i\u00e7in oturum a\u00e7\u0131n." + }, + "user": { + "data": { + "password": "\u015eifre", + "url": "Web sitesi", + "username": "Kullan\u0131c\u0131 ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/zh-Hant.json b/homeassistant/components/fireservicerota/translations/zh-Hant.json new file mode 100644 index 00000000000..af3cba40dc6 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "reauth": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8a8d\u8b49\u5bc6\u9470\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u7ad9", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ka.json b/homeassistant/components/fritzbox/translations/ka.json new file mode 100644 index 00000000000..6d43d983836 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/ka.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e8 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d0\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0" + }, + "error": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/ka.json b/homeassistant/components/gree/translations/ka.json new file mode 100644 index 00000000000..2dff3849b85 --- /dev/null +++ b/homeassistant/components/gree/translations/ka.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0", + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + }, + "step": { + "confirm": { + "description": "\u10d2\u10dc\u10d4\u10d1\u10d0\u10d5\u10d7 \u10d3\u10d0\u10d8\u10ec\u10e7\u10dd\u10d7 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/ka.json b/homeassistant/components/heos/translations/ka.json new file mode 100644 index 00000000000..a73d02d47eb --- /dev/null +++ b/homeassistant/components/heos/translations/ka.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + }, + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ka.json b/homeassistant/components/homekit/translations/ka.json new file mode 100644 index 00000000000..97787f722fc --- /dev/null +++ b/homeassistant/components/homekit/translations/ka.json @@ -0,0 +1,19 @@ +{ + "options": { + "step": { + "include_exclude": { + "data": { + "entities": "\u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8", + "mode": "\u10e0\u10d4\u10df\u10db\u10d8" + }, + "description": "\u10e8\u10d4\u10d0\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d2\u10d0\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10dc\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8. \u10d0\u10e5\u10e1\u10d4\u10e1\u10e3\u10d0\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8 \u10d2\u10d0\u10db\u10dd\u10d5\u10da\u10d4\u10dc\u10d0\u10e1 \u10d4\u10e5\u10d5\u10d4\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10d1\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8. \u10d1\u10e0\u10d8\u10ef\u10d8\u10e1 \u10db\u10dd\u10ea\u10e3\u10da\u10dd\u10d1\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8 \u10d7\u10e3 \u10e1\u10de\u10d4\u10ea\u10d8\u10e4\u10d8\u10d9\u10e3\u10e0\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8, \u10db\u10d8\u10e1\u10d0\u10ec\u10d5\u10d3\u10dd\u10db\u10d8 \u10d8\u10e5\u10dc\u10d4\u10d1\u10d0 \u10d3\u10dd\u10db\u10d4\u10dc\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8. \u10ee\u10d8\u10d3\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8, \u10d3\u10dd\u10db\u10d4\u10dc\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8 \u10d8\u10e5\u10dc\u10d4\u10d1\u10d0 \u10dc\u10d8\u10e1\u10d0\u10ec\u10d5\u10d3\u10dd\u10db\u10d8 \u10d2\u10d0\u10db\u10dd\u10e0\u10d8\u10ea\u10ee\u10e3\u10da \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10e0\u10d3\u10d0.", + "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d2\u10d0\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10dc\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8" + }, + "init": { + "data": { + "mode": "\u10e0\u10d4\u10df\u10d8\u10db\u10d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/ka.json b/homeassistant/components/iaqualink/translations/ka.json new file mode 100644 index 00000000000..552111158b9 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ka.json b/homeassistant/components/icloud/translations/ka.json new file mode 100644 index 00000000000..950b4e2f327 --- /dev/null +++ b/homeassistant/components/icloud/translations/ka.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u10e0\u10d0-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10d8\u10e7\u10dd \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8" + }, + "step": { + "reauth": { + "data": { + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8" + }, + "description": "\u10e8\u10d4\u10dc\u10d8 \u10d0\u10d3\u10e0\u10d4 \u10e8\u10d4\u10e7\u10d5\u10d0\u10dc\u10d8\u10da\u10d8 \u10de\u10d0\u10e0\u10dd\u10da\u10d8 {username} \u10d0\u10e6\u10d0\u10e0 \u10db\u10e3\u10e8\u10d0\u10dd\u10d1\u10e1. \u10d2\u10d0\u10dc\u10d0\u10d0\u10ee\u10da\u10d4\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10de\u10d0\u10e0\u10dd\u10da\u10d8, \u10e0\u10dd\u10db \u10d2\u10d0\u10dc\u10d0\u10d2\u10e0\u10eb\u10dd\u10d7 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d7 \u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10da\u10dd\u10d1\u10d0.", + "title": "\u10e0\u10d4\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ka.json b/homeassistant/components/ipp/translations/ka.json new file mode 100644 index 00000000000..44cf9de1c37 --- /dev/null +++ b/homeassistant/components/ipp/translations/ka.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + }, + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index d504b30c4bb..00e949b652b 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/ovo_energy/translations/ka.json b/homeassistant/components/ovo_energy/translations/ka.json index 7dd642123b2..1da3b36b1ec 100644 --- a/homeassistant/components/ovo_energy/translations/ka.json +++ b/homeassistant/components/ovo_energy/translations/ka.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1", "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" }, "flow_title": "OVO Energy: {username}", diff --git a/homeassistant/components/plugwise/translations/ka.json b/homeassistant/components/plugwise/translations/ka.json index f009e98e40b..d4b446b309c 100644 --- a/homeassistant/components/plugwise/translations/ka.json +++ b/homeassistant/components/plugwise/translations/ka.json @@ -1,10 +1,20 @@ { "config": { "step": { + "user": { + "data": { + "flow_type": "\u1c99\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8" + } + }, "user_gateway": { "data": { + "host": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8", + "password": "Smile ID", + "port": "\u10de\u10dd\u10e0\u10e2\u10d8", "username": "\u10e6\u10d8\u10db\u10d8\u10da\u10d8\u10d0\u10dc\u10d8 \u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8" - } + }, + "description": "\u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10dd\u10d7", + "title": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 Smile-\u10e1" } } } diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 8f2b5f94c4b..49eb2a760a4 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "external_setup": "Point configurato correttamente da un altro flusso.", - "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/ruckus_unleashed/translations/ka.json b/homeassistant/components/ruckus_unleashed/translations/ka.json new file mode 100644 index 00000000000..980dc52ca82 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/ka.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1", + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10e2\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", + "unknown": "\u10db\u10dd\u10e3\u10da\u10dd\u10d3\u10dc\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + }, + "step": { + "user": { + "data": { + "host": "\u10f0\u10dd\u10e1\u10e2\u10d8", + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8", + "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10d4\u10da\u10d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/ka.json b/homeassistant/components/tasmota/translations/ka.json new file mode 100644 index 00000000000..fd948a837c2 --- /dev/null +++ b/homeassistant/components/tasmota/translations/ka.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u10d2\u10e1\u10e3\u10e0\u10d7 \u10d3\u10d0\u10d0\u10e7\u10d4\u10dc\u10dd\u10d7 Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json index d16b69d577b..431b986fdae 100644 --- a/homeassistant/components/tellduslive/translations/it.json +++ b/homeassistant/components/tellduslive/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, "error": { "invalid_auth": "Autenticazione non valida" diff --git a/homeassistant/components/tellduslive/translations/ka.json b/homeassistant/components/tellduslive/translations/ka.json index 8e555221947..54e8ddb0b3c 100644 --- a/homeassistant/components/tellduslive/translations/ka.json +++ b/homeassistant/components/tellduslive/translations/ka.json @@ -2,6 +2,9 @@ "config": { "abort": { "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0." + }, + "error": { + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" } } } \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/ka.json b/homeassistant/components/tesla/translations/ka.json new file mode 100644 index 00000000000..249c8f6cffb --- /dev/null +++ b/homeassistant/components/tesla/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/ka.json b/homeassistant/components/tibber/translations/ka.json new file mode 100644 index 00000000000..fa4c9c0abd3 --- /dev/null +++ b/homeassistant/components/tibber/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index c7cfb228388..d757850a44f 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_agreements": "Questo account non ha display Toon.", - "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, "step": { "agreement": { diff --git a/homeassistant/components/upcloud/translations/ka.json b/homeassistant/components/upcloud/translations/ka.json new file mode 100644 index 00000000000..84d1882069a --- /dev/null +++ b/homeassistant/components/upcloud/translations/ka.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + }, + "step": { + "user": { + "data": { + "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8", + "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10d4\u10da\u10d8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8, \u10db\u10d8\u10dc\u10d8\u10db\u10e3\u10db 30" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ka.json b/homeassistant/components/withings/translations/ka.json new file mode 100644 index 00000000000..9bcce782255 --- /dev/null +++ b/homeassistant/components/withings/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ka.json b/homeassistant/components/xbox/translations/ka.json new file mode 100644 index 00000000000..dbe3de7ed4a --- /dev/null +++ b/homeassistant/components/xbox/translations/ka.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u10e3\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d4\u10d1\u10e3\u10da\u10d8 URL-\u10d8\u10e1 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10e0\u10dd\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d5\u10da\u10d0", + "missing_configuration": "\u10d9\u10dd\u10db\u10de\u10dd\u10dc\u10d4\u10dc\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10db\u10d8\u10e7\u10d4\u10d5\u10d8\u10d7 \u10d3\u10dd\u10d9\u10e3\u10db\u10d4\u10dc\u10e2\u10d0\u10ea\u10d8\u10d0\u10e1", + "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." + }, + "create_entry": { + "default": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" + }, + "step": { + "pick_implementation": { + "title": "\u10d0\u10db\u10dd\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ka.json b/homeassistant/components/xiaomi_miio/translations/ka.json new file mode 100644 index 00000000000..fa4c9c0abd3 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file From 3bd0c7188cb95f880c016ff314eef4b40446b933 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Nov 2020 01:16:16 +0100 Subject: [PATCH 255/430] Updated frontend to 20201126.0 (#43682) --- 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 aea1717ce6f..cd9d4b3b5ba 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201111.2"], + "requirements": ["home-assistant-frontend==20201126.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9465ff15124..80f74e708b6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201111.2 +home-assistant-frontend==20201126.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2ef308552a3..fced3f9cd5d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201111.2 +home-assistant-frontend==20201126.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10cbffe6e84..797d69d336f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201111.2 +home-assistant-frontend==20201126.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 69c2818c569eed5dd18b901699204e9b1750669a Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Fri, 27 Nov 2020 20:07:49 +1300 Subject: [PATCH 256/430] Met.no: Fix for zero temp entries (#43684) Addresses issue #43587 --- homeassistant/components/met/weather.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index f505d1db1a3..73b9134415d 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -217,7 +217,9 @@ class MetWeather(CoordinatorEntity, WeatherEntity): if not set(met_item).issuperset(required_keys): continue ha_item = { - k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v) + k: met_item[v] + for k, v in FORECAST_MAP.items() + if met_item.get(v) is not None } if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( From f9fa24950b0e211b959e07586e31efb09cbda2d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 08:55:34 +0100 Subject: [PATCH 257/430] OAuth2 to use current request header (#43668) --- homeassistant/components/toon/oauth2.py | 4 +- .../helpers/config_entry_oauth2_flow.py | 35 +++++++--- homeassistant/helpers/network.py | 6 +- .../tests/test_config_flow.py | 12 +++- tests/components/almond/test_config_flow.py | 12 +++- .../home_connect/test_config_flow.py | 12 +++- tests/components/nest/test_config_flow_sdm.py | 12 +++- tests/components/netatmo/test_config_flow.py | 12 +++- tests/components/smappee/test_config_flow.py | 12 +++- tests/components/somfy/test_config_flow.py | 12 +++- tests/components/spotify/test_config_flow.py | 44 +++++++++--- tests/components/toon/test_config_flow.py | 68 +++++++++++++++---- tests/components/withings/common.py | 6 +- tests/components/withings/test_config_flow.py | 8 ++- tests/components/xbox/test_config_flow.py | 12 +++- tests/conftest.py | 21 ++++-- .../helpers/test_config_entry_oauth2_flow.py | 54 +++++++-------- tests/helpers/test_network.py | 6 +- 18 files changed, 258 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py index 2622e0a9027..e3a83583ac6 100644 --- a/homeassistant/components/toon/oauth2.py +++ b/homeassistant/components/toon/oauth2.py @@ -90,8 +90,8 @@ class ToonLocalOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implemen """Initialize local Toon auth implementation.""" data = { "grant_type": "authorization_code", - "code": external_data, - "redirect_uri": self.redirect_uri, + "code": external_data["code"], + "redirect_uri": external_data["state"]["redirect_uri"], "tenant_id": self.tenant_id, } diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 4d05ad7beab..526a774cc39 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -19,9 +19,9 @@ import voluptuous as vol from yarl import URL from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView +from homeassistant.components import http from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.network import NoURLAvailableError from .aiohttp_client import async_get_clientsession @@ -32,6 +32,7 @@ DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" DATA_PROVIDERS = "oauth2_providers" AUTH_CALLBACK_PATH = "/auth/external/callback" +HEADER_FRONTEND_BASE = "HA-Frontend-Base" CLOCK_OUT_OF_SYNC_MAX_SEC = 20 @@ -64,7 +65,7 @@ class AbstractOAuth2Implementation(ABC): Pass external data in with: await hass.config_entries.flow.async_configure( - flow_id=flow_id, user_input=external_data + flow_id=flow_id, user_input={'code': 'abcd', 'state': { … } ) """ @@ -124,7 +125,17 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): @property def redirect_uri(self) -> str: """Return the redirect uri.""" - return f"{get_url(self.hass, require_current_request=True)}{AUTH_CALLBACK_PATH}" + req = http.current_request.get() + + if req is None: + raise RuntimeError("No current request in context") + + ha_host = req.headers.get(HEADER_FRONTEND_BASE) + + if ha_host is None: + raise RuntimeError("No header in request") + + return f"{ha_host}{AUTH_CALLBACK_PATH}" @property def extra_authorize_data(self) -> dict: @@ -133,14 +144,17 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): async def async_generate_authorize_url(self, flow_id: str) -> str: """Generate a url for the user to authorize.""" + redirect_uri = self.redirect_uri 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}), + "redirect_uri": redirect_uri, + "state": _encode_jwt( + self.hass, {"flow_id": flow_id, "redirect_uri": redirect_uri} + ), } ) .update_query(self.extra_authorize_data) @@ -151,8 +165,8 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): return await self._token_request( { "grant_type": "authorization_code", - "code": external_data, - "redirect_uri": self.redirect_uri, + "code": external_data["code"], + "redirect_uri": external_data["state"]["redirect_uri"], } ) @@ -384,7 +398,7 @@ def async_add_implementation_provider( ] = async_provide_implementation -class OAuth2AuthorizeCallbackView(HomeAssistantView): +class OAuth2AuthorizeCallbackView(http.HomeAssistantView): """OAuth2 Authorization Callback View.""" requires_auth = False @@ -406,7 +420,8 @@ class OAuth2AuthorizeCallbackView(HomeAssistantView): return web.Response(text="Invalid state") await hass.config_entries.flow.async_configure( - flow_id=state["flow_id"], user_input=request.query["code"] + flow_id=state["flow_id"], + user_input={"state": state, "code": request.query["code"]}, ) return web.Response( diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 3990662dc02..4e066eaa13c 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -4,7 +4,7 @@ from typing import Optional, cast import yarl -from homeassistant.components.http import current_request +from homeassistant.components import http from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass @@ -49,7 +49,7 @@ def get_url( prefer_cloud: bool = False, ) -> str: """Get a URL to this instance.""" - if require_current_request and current_request.get() is None: + if require_current_request and http.current_request.get() is None: raise NoURLAvailableError order = [TYPE_URL_INTERNAL, TYPE_URL_EXTERNAL] @@ -125,7 +125,7 @@ def get_url( def _get_request_host() -> Optional[str]: """Get the host address of the current request.""" - request = current_request.get() + request = http.current_request.get() if request is None: raise NoURLAvailableError return yarl.URL(request.url).host 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 36a42431cf3..ed974601646 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 @@ -13,7 +13,9 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -27,7 +29,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): 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"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index b2144205895..afcad55bf2a 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -91,7 +91,9 @@ async def test_abort_if_existing_entry(hass): assert result["reason"] == "single_instance_allowed" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -109,7 +111,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 5d65df98e5b..5c94f8b3362 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -14,7 +14,9 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -31,7 +33,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): result = await hass.config_entries.flow.async_init( "home_connect", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 1df751f3980..6573b17980e 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -12,7 +12,9 @@ PROJECT_ID = "project-id-4321" SUBSCRIBER_ID = "subscriber-id-9876" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -31,7 +33,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) assert result["url"] == ( diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 8cee7a8c750..74a5d8dcc92 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -42,7 +42,9 @@ async def test_abort_if_existing_entry(hass): assert result["reason"] == "already_configured" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -56,7 +58,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) scope = "+".join( [ diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 7434d469f96..55d063c2b1c 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -333,7 +333,9 @@ async def test_abort_cloud_flow_if_local_device_exists(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 -async def test_full_user_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_user_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -351,7 +353,13 @@ async def test_full_user_flow(hass, aiohttp_client, aioclient_mock, current_requ result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_CLOUD} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) client = await aiohttp_client(hass.http.app) resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 89b4fbe9b13..4276a6a18d4 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -52,7 +52,9 @@ async def test_abort_if_existing_entry(hass): assert result["reason"] == "single_instance_allowed" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -69,7 +71,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 3b3c85dd828..53e87e5bdae 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -40,7 +40,9 @@ async def test_zeroconf_abort_if_existing_entry(hass): assert result["reason"] == "already_configured" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check a full flow.""" assert await setup.async_setup_component( hass, @@ -56,7 +58,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( @@ -103,7 +111,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): async def test_abort_if_spotify_error( - hass, aiohttp_client, aioclient_mock, current_request + hass, aiohttp_client, aioclient_mock, current_request_with_host ): """Check Spotify errors causes flow to abort.""" await setup.async_setup_component( @@ -120,7 +128,13 @@ async def test_abort_if_spotify_error( ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) client = await aiohttp_client(hass.http.app) await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -144,7 +158,9 @@ async def test_abort_if_spotify_error( assert result["reason"] == "connection_error" -async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_request): +async def test_reauthentication( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Test Spotify reauthentication.""" await setup.async_setup_component( hass, @@ -173,7 +189,13 @@ async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_re result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) client = await aiohttp_client(hass.http.app) await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -202,7 +224,7 @@ async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_re async def test_reauth_account_mismatch( - hass, aiohttp_client, aioclient_mock, current_request + hass, aiohttp_client, aioclient_mock, current_request_with_host ): """Test Spotify reauthentication with different account.""" await setup.async_setup_component( @@ -230,7 +252,13 @@ async def test_reauth_account_mismatch( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) client = await aiohttp_client(hass.http.app) await client.get(f"/auth/external/callback?code=abcd&state={state}") diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 6fb7a7b53dc..b7eb3898b47 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -40,7 +40,7 @@ async def test_abort_if_no_configuration(hass): async def test_full_flow_implementation( - hass, aiohttp_client, aioclient_mock, current_request + hass, aiohttp_client, aioclient_mock, current_request_with_host ): """Test registering an integration and finishing flow works.""" await setup_component(hass) @@ -53,7 +53,13 @@ async def test_full_flow_implementation( assert result["step_id"] == "pick_implementation" # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"implementation": "eneco"} @@ -97,7 +103,9 @@ async def test_full_flow_implementation( } -async def test_no_agreements(hass, aiohttp_client, aioclient_mock, current_request): +async def test_no_agreements( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Test abort when there are no displays.""" await setup_component(hass) result = await hass.config_entries.flow.async_init( @@ -105,7 +113,13 @@ async def test_no_agreements(hass, aiohttp_client, aioclient_mock, current_reque ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) await hass.config_entries.flow.async_configure( result["flow_id"], {"implementation": "eneco"} ) @@ -130,7 +144,7 @@ async def test_no_agreements(hass, aiohttp_client, aioclient_mock, current_reque async def test_multiple_agreements( - hass, aiohttp_client, aioclient_mock, current_request + hass, aiohttp_client, aioclient_mock, current_request_with_host ): """Test abort when there are no displays.""" await setup_component(hass) @@ -139,7 +153,13 @@ async def test_multiple_agreements( ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) await hass.config_entries.flow.async_configure( result["flow_id"], {"implementation": "eneco"} ) @@ -174,7 +194,7 @@ async def test_multiple_agreements( async def test_agreement_already_set_up( - hass, aiohttp_client, aioclient_mock, current_request + hass, aiohttp_client, aioclient_mock, current_request_with_host ): """Test showing display form again if display already exists.""" await setup_component(hass) @@ -184,7 +204,13 @@ async def test_agreement_already_set_up( ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) await hass.config_entries.flow.async_configure( result["flow_id"], {"implementation": "eneco"} ) @@ -208,14 +234,22 @@ async def test_agreement_already_set_up( assert result3["reason"] == "already_configured" -async def test_toon_abort(hass, aiohttp_client, aioclient_mock, current_request): +async def test_toon_abort( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Test we abort on Toon error.""" await setup_component(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) await hass.config_entries.flow.async_configure( result["flow_id"], {"implementation": "eneco"} ) @@ -239,7 +273,7 @@ async def test_toon_abort(hass, aiohttp_client, aioclient_mock, current_request) assert result2["reason"] == "connection_error" -async def test_import(hass): +async def test_import(hass, current_request_with_host): """Test if importing step works.""" await setup_component(hass) @@ -253,7 +287,9 @@ async def test_import(hass): assert result["reason"] == "already_in_progress" -async def test_import_migration(hass, aiohttp_client, aioclient_mock, current_request): +async def test_import_migration( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Test if importing step with migration works.""" old_entry = MockConfigEntry(domain=DOMAIN, unique_id=123, version=1) old_entry.add_to_hass(hass) @@ -269,7 +305,13 @@ async def test_import_migration(hass, aiohttp_client, aioclient_mock, current_re assert flows[0]["context"][CONF_MIGRATE] == old_entry.entry_id # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": flows[0]["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flows[0]["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) await hass.config_entries.flow.async_configure( flows[0]["flow_id"], {"implementation": "eneco"} ) diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index a09876868a7..000900c3355 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -197,7 +197,11 @@ class ComponentFactory: assert result # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( - self._hass, {"flow_id": result["flow_id"]} + self._hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "http://127.0.0.1:8080/auth/external/callback", + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index cb0ea5b29ab..8380c134013 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -71,7 +71,13 @@ async def test_config_reauth_profile( ) # pylint: disable=protected-access - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) client: TestClient = await aiohttp_client(hass.http.app) resp = await client.get(f"{AUTH_CALLBACK_PATH}?code=abcd&state={state}") diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 176c5eea60a..516a57c039b 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -21,7 +21,9 @@ async def test_abort_if_existing_entry(hass): assert result["reason"] == "single_instance_allowed" -async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -35,7 +37,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): result = await hass.config_entries.flow.async_init( "xbox", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) scope = "+".join(["Xboxlive.signin", "Xboxlive.offline_access"]) diff --git a/tests/conftest.py b/tests/conftest.py index 285179e3a9b..d5c4c61ddf1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import ssl import threading from aiohttp.test_utils import make_mocked_request +import multidict import pytest import requests_mock as _requests_mock @@ -22,11 +23,11 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.http import URL from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED from homeassistant.exceptions import ServiceNotFound -from homeassistant.helpers import event +from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.async_mock import MagicMock, Mock, patch +from tests.async_mock import MagicMock, patch from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS pytest.register_assert_rewrite("tests.common") @@ -277,19 +278,29 @@ def hass_client(hass, aiohttp_client, hass_access_token): @pytest.fixture -def current_request(hass): +def current_request(): """Mock current request.""" - with patch("homeassistant.helpers.network.current_request") as mock_request_context: + with patch("homeassistant.components.http.current_request") as mock_request_context: mocked_request = make_mocked_request( "GET", "/some/request", headers={"Host": "example.com"}, sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS), ) - mock_request_context.get = Mock(return_value=mocked_request) + mock_request_context.get.return_value = mocked_request yield mock_request_context +@pytest.fixture +def current_request_with_host(current_request): + """Mock current request with a host header.""" + new_headers = multidict.CIMultiDict(current_request.get.return_value.headers) + new_headers[config_entry_oauth2_flow.HEADER_FRONTEND_BASE] = "https://example.com" + current_request.get.return_value = current_request.get.return_value.clone( + headers=new_headers + ) + + @pytest.fixture def hass_ws_client(aiohttp_client, hass_access_token, hass): """Websocket client fixture connected to websocket server.""" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 7ce71defb7e..157bbf3bc23 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -6,7 +6,6 @@ import time import pytest from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.config import async_process_ha_core_config from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.network import NoURLAvailableError @@ -146,14 +145,14 @@ async def test_abort_if_no_url_available(hass, flow_handler, local_impl): async def test_abort_if_oauth_error( - hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request + hass, + flow_handler, + local_impl, + aiohttp_client, + aioclient_mock, + current_request_with_host, ): """Check bad oauth token.""" - await async_process_ha_core_config( - hass, - {"external_url": "https://example.com"}, - ) - flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -171,7 +170,13 @@ async def test_abort_if_oauth_error( result["flow_id"], user_input={"implementation": TEST_DOMAIN} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( @@ -203,10 +208,6 @@ async def test_abort_if_oauth_error( async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" - await async_process_ha_core_config( - hass, - {"external_url": "https://example.com"}, - ) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -222,11 +223,6 @@ async def test_step_discovery(hass, flow_handler, local_impl): async def test_abort_discovered_multiple(hass, flow_handler, local_impl): """Test if aborts when discovered multiple times.""" - await async_process_ha_core_config( - hass, - {"external_url": "https://example.com"}, - ) - flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -249,10 +245,6 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl): """Test if abort discovery when entries exists.""" - await async_process_ha_core_config( - hass, - {"external_url": "https://example.com"}, - ) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -273,14 +265,14 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl) async def test_full_flow( - hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request + hass, + flow_handler, + local_impl, + aiohttp_client, + aioclient_mock, + current_request_with_host, ): """Check full flow.""" - await async_process_ha_core_config( - hass, - {"external_url": "https://example.com"}, - ) - flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -298,7 +290,13 @@ async def test_full_flow( result["flow_id"], user_input={"implementation": TEST_DOMAIN} ) - state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 495c9d511bd..c470fbd7834 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -500,7 +500,7 @@ async def test_get_url(hass: HomeAssistant): with patch( "homeassistant.helpers.network._get_request_host", return_value="example.com" - ), patch("homeassistant.helpers.network.current_request"): + ), patch("homeassistant.components.http.current_request"): assert get_url(hass, require_current_request=True) == "https://example.com" assert ( get_url(hass, require_current_request=True, require_ssl=True) @@ -512,7 +512,7 @@ async def test_get_url(hass: HomeAssistant): with patch( "homeassistant.helpers.network._get_request_host", return_value="example.local" - ), patch("homeassistant.helpers.network.current_request"): + ), patch("homeassistant.components.http.current_request"): assert get_url(hass, require_current_request=True) == "http://example.local" with pytest.raises(NoURLAvailableError): @@ -533,7 +533,7 @@ async def test_get_request_host(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_request_host() - with patch("homeassistant.helpers.network.current_request") as mock_request_context: + with patch("homeassistant.components.http.current_request") as mock_request_context: mock_request = Mock() mock_request.url = "http://example.com:8123/test/request" mock_request_context.get = Mock(return_value=mock_request) From a5e5c5c7e6407ff0d7c9756a92c31b9776168712 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 Nov 2020 09:03:44 +0100 Subject: [PATCH 258/430] Exclude disabled entities from async_entries_for_device (#43665) --- homeassistant/components/deconz/services.py | 9 ++++- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/debug_info.py | 2 +- homeassistant/components/tasmota/discovery.py | 4 ++- homeassistant/components/tuya/__init__.py | 2 +- .../components/unifi/unifi_entity_base.py | 11 +++++- .../components/zha/core/discovery.py | 4 ++- homeassistant/components/zha/core/gateway.py | 4 ++- homeassistant/components/zha/core/group.py | 4 ++- homeassistant/helpers/entity_registry.py | 15 +++++--- homeassistant/helpers/service.py | 2 +- tests/helpers/test_entity_registry.py | 36 +++++++++++++++++++ 12 files changed, 81 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index a90f770eb9b..65b9c1aad8a 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -207,5 +207,12 @@ async def async_remove_orphaned_entries_service(hass, data): # Remove devices that don't belong to any entity for device_id in devices_to_be_removed: - if len(async_entries_for_device(entity_registry, device_id)) == 0: + if ( + len( + async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True + ) + ) + == 0 + ): device_registry.async_remove_device(device_id) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 27b43142f7c..bd89a2364be 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1248,7 +1248,7 @@ async def cleanup_device_registry(hass, device_id): if ( device_id and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id + entity_registry, device_id, include_disabled_entities=True ) and not await device_trigger.async_get_triggers(hass, device_id) and not tag.async_has_tags(hass, device_id) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 75e4b53a191..a3c56652253 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -123,7 +123,7 @@ async def info_for_device(hass, device_id): entity_registry = await hass.helpers.entity_registry.async_get_registry() entries = hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id + entity_registry, device_id, include_disabled_entities=True ) mqtt_debug_info = hass.data.setdefault( DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 2313a8327c5..22824e9cd71 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -144,7 +144,9 @@ async def async_start( orphaned_entities = { entry.unique_id - for entry in async_entries_for_device(entity_registry, device.id) + for entry in async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) if entry.domain == sensor.DOMAIN and entry.platform == DOMAIN } for (tasmota_sensor_config, discovery_hash) in sensors: diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 64a2d203695..bc665baeb86 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -269,7 +269,7 @@ async def cleanup_device_registry(hass: HomeAssistant, device_id): device_registry = await hass.helpers.device_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry() if device_id and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id + entity_registry, device_id, include_disabled_entities=True ): device_registry.async_remove_device(device_id) diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index a730c134603..7b45d309c14 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -95,7 +95,16 @@ class UniFiBase(Entity): entity_registry.async_remove(self.entity_id) return - if len(async_entries_for_device(entity_registry, entity_entry.device_id)) == 1: + if ( + len( + async_entries_for_device( + entity_registry, + entity_entry.device_id, + include_disabled_entities=True, + ) + ) + == 1 + ): device_registry.async_remove_device(device_entry.id) return diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 4dff2c6b16b..05a12bc2284 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -243,7 +243,9 @@ class GroupProbe: if member.device.is_coordinator: continue entities = async_entries_for_device( - zha_gateway.ha_entity_registry, member.device.device_id + zha_gateway.ha_entity_registry, + member.device.device_id, + include_disabled_entities=True, ) all_domain_occurrences.extend( [ diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 69bf85e47a4..13a87c13226 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -400,7 +400,9 @@ class ZHAGateway: # then we get all group entity entries tied to the coordinator all_group_entity_entries = async_entries_for_device( - self.ha_entity_registry, self.coordinator_zha_device.device_id + self.ha_entity_registry, + self.coordinator_zha_device.device_id, + include_disabled_entities=True, ) # then we get the entity entries for this specific group by getting the entries that match diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 2961f335989..8edb1da8f68 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -195,7 +195,9 @@ class ZHAGroup(LogMixin): domain_entity_ids: List[str] = [] for member in self.members: entities = async_entries_for_device( - self._zha_gateway.ha_entity_registry, member.device.device_id + self._zha_gateway.ha_entity_registry, + member.device.device_id, + include_disabled_entities=True, ) domain_entity_ids.extend( [entity.entity_id for entity in entities if entity.domain == domain] diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 143f3a99137..7e8700e8236 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -298,7 +298,9 @@ class EntityRegistry: the device is disabled. """ if event.data["action"] == "remove": - entities = async_entries_for_device(self, event.data["device_id"]) + entities = async_entries_for_device( + self, event.data["device_id"], include_disabled_entities=True + ) for entity in entities: self.async_remove(entity.entity_id) return @@ -311,7 +313,9 @@ class EntityRegistry: if not device.disabled: return - entities = async_entries_for_device(self, event.data["device_id"]) + entities = async_entries_for_device( + self, event.data["device_id"], include_disabled_entities=True + ) for entity in entities: self.async_update_entity( # type: ignore entity.entity_id, disabled_by=DISABLED_DEVICE @@ -548,11 +552,14 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: @callback def async_entries_for_device( - registry: EntityRegistry, device_id: str + registry: EntityRegistry, device_id: str, include_disabled_entities: bool = False ) -> List[RegistryEntry]: """Return entries that match a device.""" return [ - entry for entry in registry.entities.values() if entry.device_id == device_id + entry + for entry in registry.entities.values() + if entry.device_id == device_id + and (not entry.disabled_by or include_disabled_entities) ] diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 6a290a77b08..e6c805e9a35 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -260,7 +260,7 @@ async def async_extract_entity_ids( entry.entity_id for device in devices for entry in hass.helpers.entity_registry.async_entries_for_device( - ent_reg, device.id + ent_reg, device.id, include_disabled_entities=True ) if not entry.area_id ) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index f42661ec915..960537e784c 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -736,3 +736,39 @@ async def test_disable_device_disables_entities(hass, registry): entry = registry.async_get(entry.entity_id) assert entry.disabled assert entry.disabled_by == "device" + + +async def test_disabled_entities_excluded_from_entity_list(hass, registry): + """Test that disabled entities are exclduded from async_entries_for_device.""" + device_registry = mock_device_registry(hass) + config_entry = MockConfigEntry(domain="light") + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + + entry1 = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + entry2 = registry.async_get_or_create( + "light", + "hue", + "ABCD", + config_entry=config_entry, + device_id=device_entry.id, + disabled_by="user", + ) + + entries = entity_registry.async_entries_for_device(registry, device_entry.id) + assert entries == [entry1] + + entries = entity_registry.async_entries_for_device( + registry, device_entry.id, include_disabled_entities=True + ) + assert entries == [entry1, entry2] From bf4e6a289a8917091e6de031c167e40bbc7c0d75 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 27 Nov 2020 09:13:16 +0100 Subject: [PATCH 259/430] Add option to deactivate a user (#43463) --- homeassistant/auth/__init__.py | 7 +++ homeassistant/components/config/auth.py | 11 ++++ tests/components/config/test_auth.py | 73 +++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index d2ce63f9490..e36eb6800fa 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -284,6 +284,7 @@ class AuthManager: self, user: models.User, name: Optional[str] = None, + is_active: Optional[bool] = None, group_ids: Optional[List[str]] = None, ) -> None: """Update a user.""" @@ -294,6 +295,12 @@ class AuthManager: kwargs["group_ids"] = group_ids await self._store.async_update_user(user, **kwargs) + if is_active is not None: + if is_active is True: + await self.async_activate_user(user) + else: + await self.async_deactivate_user(user) + async def async_activate_user(self, user: models.User) -> None: """Activate a user.""" await self._store.async_activate_user(user) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 440ac05cef4..c1d43a5d4a9 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -86,6 +86,7 @@ async def websocket_create(hass, connection, msg): vol.Required("type"): "config/auth/update", vol.Required("user_id"): str, vol.Optional("name"): str, + vol.Optional("is_active"): bool, vol.Optional("group_ids"): [str], } ) @@ -111,6 +112,16 @@ async def websocket_update(hass, connection, msg): ) return + if user.is_owner and msg["is_active"] is False: + connection.send_message( + websocket_api.error_message( + msg["id"], + "cannot_deactivate_owner", + "Unable to deactivate owner.", + ) + ) + return + msg.pop("type") msg_id = msg.pop("id") diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index e7e3c67e5d8..2d3cfe54f5a 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -285,3 +285,76 @@ async def test_update_system_generated(hass, hass_ws_client): assert not result["success"], result assert result["error"]["code"] == "cannot_modify_system_generated" assert user.name == "Test user" + + +async def test_deactivate(hass, hass_ws_client): + """Test deactivation and reactivation of regular user.""" + client = await hass_ws_client(hass) + + user = await hass.auth.async_create_user("Test user") + assert user.is_active is True + + await client.send_json( + { + "id": 5, + "type": "config/auth/update", + "user_id": user.id, + "name": "Updated name", + "is_active": False, + } + ) + + result = await client.receive_json() + assert result["success"], result + data_user = result["result"]["user"] + assert data_user["is_active"] is False + + await client.send_json( + { + "id": 6, + "type": "config/auth/update", + "user_id": user.id, + "name": "Updated name", + "is_active": True, + } + ) + + result = await client.receive_json() + assert result["success"], result + data_user = result["result"]["user"] + assert data_user["is_active"] is True + + +async def test_deactivate_owner(hass, hass_ws_client): + """Test that owner cannot be deactivated.""" + user = MockUser(id="abc", name="Test Owner", is_owner=True).add_to_hass(hass) + + assert user.is_active is True + assert user.is_owner is True + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "config/auth/update", "user_id": user.id, "is_active": False} + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "cannot_deactivate_owner" + + +async def test_deactivate_system_generated(hass, hass_ws_client): + """Test that owner cannot be deactivated.""" + client = await hass_ws_client(hass) + + user = await hass.auth.async_create_system_user("Test user") + assert user.is_active is True + assert user.system_generated is True + assert user.is_owner is False + + await client.send_json( + {"id": 5, "type": "config/auth/update", "user_id": user.id, "is_active": False} + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "cannot_modify_system_generated" From c2cc60534348f25e4e3beb24b4217e9d9bddfef6 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 27 Nov 2020 08:20:20 +0000 Subject: [PATCH 260/430] Bugfix schedule assigned to wrong day of week (#43676) --- 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 958750c4dcb..841b9b02055 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -635,7 +635,7 @@ class EvoChild(EvoDevice): return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints day_time = dt_util.now() - day_of_week = int(day_time.strftime("%w")) # 0 is Sunday + day_of_week = day_time.weekday() # for evohome, 0 is Monday time_of_day = day_time.strftime("%H:%M:%S") try: From 5e3f4954f7b804d43db7a4148f967838057f454d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 27 Nov 2020 08:28:05 +0000 Subject: [PATCH 261/430] Code quality improvement for evohome (#43678) --- homeassistant/components/evohome/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 841b9b02055..08d5666dcd8 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -145,7 +145,6 @@ def _handle_exception(err) -> bool: "Message is: %s", err, ) - return False except aiohttp.ClientConnectionError: # this appears to be a common occurrence with the vendor's servers @@ -155,7 +154,6 @@ def _handle_exception(err) -> bool: "Message is: %s", err, ) - return False except aiohttp.ClientResponseError: if err.status == HTTP_SERVICE_UNAVAILABLE: @@ -163,17 +161,16 @@ def _handle_exception(err) -> bool: "The vendor says their server is currently unavailable. " "Check the vendor's service status page" ) - return False - if err.status == HTTP_TOO_MANY_REQUESTS: + elif err.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " "If this message persists, consider increasing the %s", CONF_SCAN_INTERVAL, ) - return False - raise # we don't expect/handle any other Exceptions + else: + raise # we don't expect/handle any other Exceptions async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: @@ -427,8 +424,8 @@ class EvoBroker: try: result = await api_function except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - if not _handle_exception(err): - return + _handle_exception(err) + return if update_state: # wait a moment for system to quiesce before updating state self.hass.helpers.event.async_call_later(1, self._update_v2_api_state) From 2498340e1fb3890a566fe7325ed949c932c807c2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 27 Nov 2020 10:40:06 +0200 Subject: [PATCH 262/430] Fix Shelly uptime sensor (#43651) Fix sensor to include time zone Report new value only if delta > 5 seconds Modify REST sensors class to use callable attributes --- .../components/shelly/binary_sensor.py | 12 ++-- homeassistant/components/shelly/entity.py | 56 +++++++++---------- homeassistant/components/shelly/sensor.py | 6 +- homeassistant/components/shelly/utils.py | 34 +++++------ 4 files changed, 49 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 62cd9aea8ce..53038352d4d 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -75,19 +75,19 @@ SENSORS = { REST_SENSORS = { "cloud": RestAttributeDescription( name="Cloud", + value=lambda status, _: status["cloud"]["connected"], device_class=DEVICE_CLASS_CONNECTIVITY, default_enabled=False, - path="cloud/connected", ), "fwupdate": RestAttributeDescription( name="Firmware update", icon="mdi:update", + value=lambda status, _: status["update"]["has_update"], default_enabled=False, - path="update/has_update", - attributes=[ - {"description": "latest_stable_version", "path": "update/new_version"}, - {"description": "installed_version", "path": "update/old_version"}, - ], + device_state_attributes=lambda status: { + "latest_stable_version": status["update"]["new_version"], + "installed_version": status["update"]["old_version"], + }, ), } diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index b99f32be783..b4df2d486f8 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers import device_registry, entity, update_coordinator from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST -from .utils import async_remove_shelly_entity, get_entity_name, get_rest_value_from_path +from .utils import async_remove_shelly_entity, get_entity_name async def async_setup_entry_attribute_entities( @@ -64,15 +64,20 @@ async def async_setup_entry_rest( entities = [] for sensor_id in sensors: - _desc = sensors.get(sensor_id) + description = sensors.get(sensor_id) if not wrapper.device.settings.get("sleep_mode"): - entities.append(_desc) + entities.append((sensor_id, description)) if not entities: return - async_add_entities([sensor_class(wrapper, description) for description in entities]) + async_add_entities( + [ + sensor_class(wrapper, sensor_id, description) + for sensor_id, description in entities + ] + ) @dataclass @@ -98,15 +103,13 @@ class BlockAttributeDescription: class RestAttributeDescription: """Class to describe a REST sensor.""" - path: str name: str - # Callable = lambda attr_info: unit icon: Optional[str] = None - unit: Union[None, str, Callable[[dict], str]] = None - value: Callable[[Any], Any] = lambda val: val + unit: Optional[str] = None + value: Callable[[dict, Any], Any] = None device_class: Optional[str] = None default_enabled: bool = True - attributes: Optional[dict] = None + device_state_attributes: Optional[Callable[[dict], Optional[dict]]] = None class ShellyBlockEntity(entity.Entity): @@ -247,17 +250,18 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): """Class to load info from REST.""" def __init__( - self, wrapper: ShellyDeviceWrapper, description: RestAttributeDescription + self, + wrapper: ShellyDeviceWrapper, + attribute: str, + description: RestAttributeDescription, ) -> None: """Initialize sensor.""" super().__init__(wrapper) self.wrapper = wrapper + self.attribute = attribute self.description = description - - self._unit = self.description.unit self._name = get_entity_name(wrapper.device, None, self.description.name) - self.path = self.description.path - self._attributes = self.description.attributes + self._last_value = None @property def name(self): @@ -283,10 +287,11 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): @property def attribute_value(self): - """Attribute.""" - return get_rest_value_from_path( - self.wrapper.device.status, self.description.device_class, self.path + """Value of sensor.""" + self._last_value = self.description.value( + self.wrapper.device.status, self._last_value ) + return self._last_value @property def unit_of_measurement(self): @@ -306,23 +311,12 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): @property def unique_id(self): """Return unique ID of entity.""" - return f"{self.wrapper.mac}-{self.description.path}" + return f"{self.wrapper.mac}-{self.attribute}" @property def device_state_attributes(self) -> dict: """Return the state attributes.""" - - if self._attributes is None: + if self.description.device_state_attributes is None: return None - attributes = dict() - for attrib in self._attributes: - description = attrib.get("description") - attribute_value = get_rest_value_from_path( - self.wrapper.device.status, - self.description.device_class, - attrib.get("path"), - ) - attributes[description] = attribute_value - - return attributes + return self.description.device_state_attributes(self.wrapper.device.status) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index f4dfd16aa25..b92b90c1b46 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -21,7 +21,7 @@ from .entity import ( async_setup_entry_attribute_entities, async_setup_entry_rest, ) -from .utils import temperature_unit +from .utils import get_device_uptime, temperature_unit SENSORS = { ("device", "battery"): BlockAttributeDescription( @@ -170,15 +170,15 @@ REST_SENSORS = { "rssi": RestAttributeDescription( name="RSSI", unit=SIGNAL_STRENGTH_DECIBELS, + value=lambda status, _: status["wifi_sta"]["rssi"], device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, default_enabled=False, - path="wifi_sta/rssi", ), "uptime": RestAttributeDescription( name="Uptime", + value=get_device_uptime, device_class=sensor.DEVICE_CLASS_TIMESTAMP, default_enabled=False, - path="uptime", ), } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 7c72f262716..976afdd755b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -1,13 +1,13 @@ """Shelly helpers functions.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from typing import Optional import aioshelly -from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.util.dt import parse_datetime, utcnow from .const import DOMAIN @@ -81,23 +81,6 @@ def get_entity_name( return entity_name -def get_rest_value_from_path(status, device_class, path: str): - """Parser for REST path from device status.""" - - if "/" not in path: - attribute_value = status[path] - else: - attribute_value = status[path.split("/")[0]][path.split("/")[1]] - if device_class == DEVICE_CLASS_TIMESTAMP: - last_boot = datetime.utcnow() - timedelta(seconds=attribute_value) - attribute_value = last_boot.replace(microsecond=0).isoformat() - - if "new_version" in path: - attribute_value = attribute_value.split("/")[1].split("@")[0] - - return attribute_value - - def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: """Return true if input button settings is set to a momentary type.""" button = settings.get("relays") or settings.get("lights") or settings.get("inputs") @@ -112,3 +95,16 @@ def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: button_type = button[channel].get("btn_type") return button_type in ["momentary", "momentary_on_release"] + + +def get_device_uptime(status: dict, last_uptime: str) -> str: + """Return device uptime string, tolerate up to 5 seconds deviation.""" + uptime = utcnow() - timedelta(seconds=status["uptime"]) + + if not last_uptime: + return uptime.replace(microsecond=0).isoformat() + + if abs((uptime - parse_datetime(last_uptime)).total_seconds()) > 5: + return uptime.replace(microsecond=0).isoformat() + + return last_uptime From 3a17e22cfa6c13a6bc42ec5278357e8485ca2975 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 09:50:19 +0100 Subject: [PATCH 263/430] Convert API integration to async setup (#43685) --- homeassistant/components/api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 72fb2636067..f383f982abc 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -56,7 +56,7 @@ STREAM_PING_PAYLOAD = "ping" STREAM_PING_INTERVAL = 50 # seconds -def setup(hass, config): +async def async_setup(hass, config): """Register the API with the HTTP interface.""" hass.http.register_view(APIStatusView) hass.http.register_view(APIEventStream) From bb146680ce9c75377d7929927c359b220690be89 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 27 Nov 2020 11:05:51 +0000 Subject: [PATCH 264/430] Eliminate evohome unhandled exceptions when client API call fails (#43681) --- homeassistant/components/evohome/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 08d5666dcd8..268e7709af3 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -34,7 +34,7 @@ from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET +from .const import DOMAIN, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET _LOGGER = logging.getLogger(__name__) @@ -628,8 +628,8 @@ class EvoChild(EvoDevice): dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset return dt_util.as_local(dt_aware) - if not self._schedule["DailySchedules"]: - return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints + if not self._schedule or not self._schedule.get("DailySchedules"): + return {} # no scheduled setpoints when {'DailySchedules': []} day_time = dt_util.now() day_of_week = day_time.weekday() # for evohome, 0 is Monday @@ -679,10 +679,6 @@ class EvoChild(EvoDevice): async def _update_schedule(self) -> None: """Get the latest schedule, if any.""" - 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._evo_broker.call_client_api( self._evo_device.schedule(), update_state=False ) From 897ff31ffe5dd44c5b22514bbd8fdd262a783111 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 12:31:42 +0100 Subject: [PATCH 265/430] Maybe fix flaky test (#43690) --- tests/components/shelly/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1796847bd74..bef018e7d91 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch( + with patch("aioshelly.COAP", return_value=Mock(initialize=AsyncMock())), patch( "aioshelly.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ), patch( From ea55051161ee5443c9df60ae8b601478800bc02f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 12:53:16 +0100 Subject: [PATCH 266/430] Stub finding custom integrations in tests (#43692) --- tests/common.py | 2 ++ tests/conftest.py | 6 ++++++ tests/test_loader.py | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/common.py b/tests/common.py index dbe2cbfd42a..66303ad96b3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -195,6 +195,8 @@ async def async_test_home_assistant(loop): hass.async_add_executor_job = async_add_executor_job hass.async_create_task = async_create_task + hass.data[loader.DATA_CUSTOM_COMPONENTS] = {} + hass.config.location_name = "test home" hass.config.config_dir = get_test_config_dir() hass.config.latitude = 32.87336 diff --git a/tests/conftest.py b/tests/conftest.py index d5c4c61ddf1..fa390f9bf3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -537,3 +537,9 @@ def legacy_patchable_time(): async_track_utc_time_change, ): yield + + +@pytest.fixture +def enable_custom_integrations(hass): + """Enable custom integrations defined in the test dir.""" + hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) diff --git a/tests/test_loader.py b/tests/test_loader.py index 71a373a579d..69e6688b9de 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -149,7 +149,7 @@ async def test_get_integration_legacy(hass): assert integration.get_platform("switch") is not None -async def test_get_integration_custom_component(hass): +async def test_get_integration_custom_component(hass, enable_custom_integrations): """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_package") print(integration) @@ -293,7 +293,7 @@ def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): ) -async def test_get_custom_components(hass): +async def test_get_custom_components(hass, enable_custom_integrations): """Verify that custom components are cached.""" test_1_integration = _get_test_integration(hass, "test_1", False) test_2_integration = _get_test_integration(hass, "test_2", True) From bdb04dcb9d455823a771b21318f786317b0f92ee Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Fri, 27 Nov 2020 04:39:26 -0800 Subject: [PATCH 267/430] Add Abode MFA support (#43572) --- homeassistant/components/abode/__init__.py | 26 ++- homeassistant/components/abode/config_flow.py | 164 ++++++++++++++---- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/abode/strings.json | 20 ++- .../components/abode/translations/en.json | 17 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/abode/test_config_flow.py | 112 ++++++++++-- tests/components/abode/test_init.py | 32 ++++ 9 files changed, 318 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 2ac52c87131..529e3ff7189 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -4,12 +4,12 @@ from copy import deepcopy from functools import partial from abodepy import Abode -from abodepy.exceptions import AbodeException +from abodepy.exceptions import AbodeAuthenticationException, 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.config_entries import SOURCE_IMPORT, SOURCE_REAUTH from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, @@ -110,18 +110,34 @@ async def async_setup_entry(hass, config_entry): username = config_entry.data.get(CONF_USERNAME) password = config_entry.data.get(CONF_PASSWORD) polling = config_entry.data.get(CONF_POLLING) + cache = hass.config.path(DEFAULT_CACHEDB) + + # For previous config entries where unique_id is None + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=config_entry.data[CONF_USERNAME] + ) try: - cache = hass.config.path(DEFAULT_CACHEDB) abode = await hass.async_add_executor_job( Abode, username, password, True, True, True, cache ) - hass.data[DOMAIN] = AbodeSystem(abode, polling) + + except AbodeAuthenticationException as ex: + LOGGER.error("Invalid credentials: %s", ex) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=config_entry.data, + ) + return False except (AbodeException, ConnectTimeout, HTTPError) as ex: - LOGGER.error("Unable to connect to Abode: %s", str(ex)) + LOGGER.error("Unable to connect to Abode: %s", ex) raise ConfigEntryNotReady from ex + hass.data[DOMAIN] = AbodeSystem(abode, polling) + for platform in ABODE_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 72e7ec1d9eb..76c23f7f705 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -1,15 +1,16 @@ """Config flow for the Abode Security System component.""" from abodepy import Abode -from abodepy.exceptions import AbodeException +from abodepy.exceptions import AbodeAuthenticationException, AbodeException +from abodepy.helpers.errors import MFA_CODE_REQUIRED from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST -from homeassistant.core import callback from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import +CONF_MFA = "mfa_code" CONF_POLLING = "polling" @@ -25,53 +26,146 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } + self.mfa_data_schema = { + vol.Required(CONF_MFA): str, + } + + self._cache = None + self._mfa_code = None + self._password = None + self._polling = False + self._username = None + + async def _async_abode_login(self, step_id): + """Handle login with Abode.""" + self._cache = self.hass.config.path(DEFAULT_CACHEDB) + errors = {} + + try: + await self.hass.async_add_executor_job( + Abode, self._username, self._password, True, False, False, self._cache + ) + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + if ex.errcode == MFA_CODE_REQUIRED[0]: + return await self.async_step_mfa() + + LOGGER.error("Unable to connect to Abode: %s", ex) + + if ex.errcode == HTTP_BAD_REQUEST: + errors = {"base": "invalid_auth"} + + else: + errors = {"base": "cannot_connect"} + + if errors: + return self.async_show_form( + step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors + ) + + return await self._async_create_entry() + + async def _async_abode_mfa_login(self): + """Handle multi-factor authentication (MFA) login with Abode.""" + try: + # Create instance to access login method for passing MFA code + abode = Abode( + auto_login=False, + get_devices=False, + get_automations=False, + cache_path=self._cache, + ) + await self.hass.async_add_executor_job( + abode.login, self._username, self._password, self._mfa_code + ) + + except AbodeAuthenticationException: + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema(self.mfa_data_schema), + errors={"base": "invalid_mfa_code"}, + ) + + return await self._async_create_entry() + + async def _async_create_entry(self): + """Create the config entry.""" + config_data = { + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_POLLING: self._polling, + } + existing_entry = await self.async_set_unique_id(self._username) + + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=config_data + ) + # Reload the Abode config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=self._username, data=config_data) 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) - cache = self.hass.config.path(DEFAULT_CACHEDB) - - try: - await self.hass.async_add_executor_job( - Abode, username, password, True, True, True, cache + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=vol.Schema(self.data_schema) ) - except (AbodeException, ConnectTimeout, HTTPError) as ex: - LOGGER.error("Unable to connect to Abode: %s", str(ex)) - if ex.errcode == HTTP_BAD_REQUEST: - return self._show_form({"base": "invalid_auth"}) - return self._show_form({"base": "cannot_connect"}) + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_POLLING: polling, - }, - ) + return await self._async_abode_login(step_id="user") - @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_mfa(self, user_input=None): + """Handle a multi-factor authentication (MFA) flow.""" + if user_input is None: + return self.async_show_form( + step_id="mfa", data_schema=vol.Schema(self.mfa_data_schema) + ) + + self._mfa_code = user_input[CONF_MFA] + + return await self._async_abode_mfa_login() + + async def async_step_reauth(self, config): + """Handle reauthorization request from Abode.""" + self._username = config[CONF_USERNAME] + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauthorization flow.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME, default=self._username): str, + vol.Required(CONF_PASSWORD): str, + } + ), + ) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + return await self._async_abode_login(step_id="reauth_confirm") 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.") + LOGGER.warning("Already configured. Only a single configuration possible.") return self.async_abort(reason="single_instance_allowed") + self._polling = import_config.get(CONF_POLLING, False) + return await self.async_step_user(import_config) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index e9a871035e6..b7c962dac38 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,7 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["abodepy==1.1.0"], + "requirements": ["abodepy==1.2.0"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json index 63b62fefcec..14a60f827c3 100644 --- a/homeassistant/components/abode/strings.json +++ b/homeassistant/components/abode/strings.json @@ -7,14 +7,30 @@ "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "mfa": { + "title": "Enter your MFA code for Abode", + "data": { + "mfa_code": "MFA code (6-digits)" + } + }, + "reauth_confirm": { + "title": "Fill in your Abode login information", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_mfa_code": "Invalid MFA code" + }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } \ No newline at end of file diff --git a/homeassistant/components/abode/translations/en.json b/homeassistant/components/abode/translations/en.json index 36f8bbb10e4..c1deaf0a00c 100644 --- a/homeassistant/components/abode/translations/en.json +++ b/homeassistant/components/abode/translations/en.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Re-authentication was successful", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "invalid_mfa_code": "Invalid MFA code" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA code (6-digits)" + }, + "title": "Enter your MFA code for Abode" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Email" + }, + "title": "Fill in your Abode login information" + }, "user": { "data": { "password": "Password", diff --git a/requirements_all.txt b/requirements_all.txt index fced3f9cd5d..0e745d9995f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -93,7 +93,7 @@ WSDiscovery==2.0.0 WazeRouteCalculator==0.12 # homeassistant.components.abode -abodepy==1.1.0 +abodepy==1.2.0 # homeassistant.components.accuweather accuweather==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 797d69d336f..c858aea7be3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ RtmAPI==0.7.2 WSDiscovery==2.0.0 # homeassistant.components.abode -abodepy==1.1.0 +abodepy==1.2.0 # homeassistant.components.accuweather accuweather==0.0.11 diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 509a68eda4b..f1445db340f 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,9 +1,17 @@ """Tests for the Abode config flow.""" from abodepy.exceptions import AbodeAuthenticationException +from abodepy.helpers.errors import MFA_CODE_REQUIRED from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERVER_ERROR +from homeassistant.components.abode.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BAD_REQUEST, + HTTP_INTERNAL_SERVER_ERROR, +) from tests.async_mock import patch from tests.common import MockConfigEntry @@ -28,7 +36,7 @@ async def test_one_config_allowed(hass): flow.hass = hass MockConfigEntry( - domain="abode", + domain=DOMAIN, data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, ).add_to_hass(hass) @@ -58,7 +66,7 @@ async def test_invalid_credentials(hass): with patch( "homeassistant.components.abode.config_flow.Abode", - side_effect=AbodeAuthenticationException((400, "auth error")), + side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")), ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {"base": "invalid_auth"} @@ -89,13 +97,13 @@ async def test_step_import(hass): 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) + with patch("homeassistant.components.abode.config_flow.Abode"), patch( + "abodepy.UTILS" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=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", @@ -108,11 +116,14 @@ 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"), patch( + "abodepy.UTILS" + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) - 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"] == { @@ -120,3 +131,78 @@ async def test_step_user(hass): CONF_PASSWORD: "password", CONF_POLLING: False, } + + +async def test_step_mfa(hass): + """Test that the MFA step works.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException(MFA_CODE_REQUIRED), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mfa" + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "invalid mfa")), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"mfa_code": "123456"} + ) + + assert result["errors"] == {"base": "invalid_mfa_code"} + + with patch("homeassistant.components.abode.config_flow.Abode"), patch( + "abodepy.UTILS" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"mfa_code": "123456"} + ) + + 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, + } + + +async def test_step_reauth(hass): + """Test the reauth flow.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + MockConfigEntry( + domain=DOMAIN, + unique_id="user@email.com", + data=conf, + ).add_to_hass(hass) + + with patch("homeassistant.components.abode.config_flow.Abode"), patch( + "abodepy.UTILS" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=conf, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=conf, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 1598e7bfa91..68f7ce9dd03 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,4 +1,6 @@ """Tests for the Abode module.""" +from abodepy.exceptions import AbodeAuthenticationException + from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -6,6 +8,7 @@ from homeassistant.components.abode import ( SERVICE_TRIGGER_AUTOMATION, ) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform @@ -27,6 +30,22 @@ async def test_change_settings(hass): mock_set_setting.assert_called_once() +async def test_add_unique_id(hass): + """Test unique_id is set to Abode username.""" + mock_entry = await setup_platform(hass, ALARM_DOMAIN) + # Set unique_id to None to match previous config entries + hass.config_entries.async_update_entry(entry=mock_entry, unique_id=None) + await hass.async_block_till_done() + + assert mock_entry.unique_id is None + + with patch("abodepy.UTILS"): + await hass.config_entries.async_reload(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.unique_id == mock_entry.data[CONF_USERNAME] + + async def test_unload_entry(hass): """Test unloading the Abode entry.""" mock_entry = await setup_platform(hass, ALARM_DOMAIN) @@ -41,3 +60,16 @@ async def test_unload_entry(hass): assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_SETTINGS) assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE) assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION) + + +async def test_invalid_credentials(hass): + """Test Abode credentials changing.""" + with patch( + "homeassistant.components.abode.Abode", + side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")), + ), patch( + "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth" + ) as mock_async_step_reauth: + await setup_platform(hass, ALARM_DOMAIN) + + mock_async_step_reauth.assert_called_once() From d34753473d6c6712ad3990ea9c4f8ba35df7ee84 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 16:12:39 +0100 Subject: [PATCH 268/430] Use run_job for service helper (#43696) --- homeassistant/helpers/service.py | 4 ++-- tests/components/input_boolean/test_init.py | 18 +++++++----------- tests/components/input_datetime/test_init.py | 3 --- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e6c805e9a35..c3700581b40 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -531,7 +531,7 @@ async def _handle_entity_call( else: result = hass.async_run_job(func, entity, data) - # Guard because callback functions do not return a task when passed to async_add_job. + # Guard because callback functions do not return a task when passed to async_run_job. if result is not None: await result @@ -564,7 +564,7 @@ def async_register_admin_service( if not user.is_admin: raise Unauthorized(context=call.context) - result = hass.async_add_job(service_func, call) + result = hass.async_run_job(service_func, call) if result is not None: await result diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 1e3150e687a..9911ced28dd 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -63,23 +63,21 @@ async def test_methods(hass): assert not is_on(hass, entity_id) - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) - - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) assert is_on(hass, entity_id) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id} + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) - await hass.async_block_till_done() - assert not is_on(hass, entity_id) - await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) assert is_on(hass, entity_id) @@ -246,7 +244,6 @@ async def test_reload(hass, hass_admin_user): 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()) @@ -349,6 +346,5 @@ async def test_setup_no_config(hass, hass_admin_user): blocking=True, context=Context(user_id=hass_admin_user.id), ) - await hass.async_block_till_done() assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 53914a83ba2..d40a88e3f43 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -276,7 +276,6 @@ async def test_set_invalid_2(hass): {"entity_id": entity_id, "time": time_portion, "datetime": dt_obj}, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == initial @@ -467,7 +466,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): 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()) @@ -643,7 +641,6 @@ async def test_setup_no_config(hass, hass_admin_user): blocking=True, context=Context(user_id=hass_admin_user.id), ) - await hass.async_block_till_done() assert count_start == len(hass.states.async_entity_ids()) From 20ed40d7ade436eef1fcf25b5468bb3163434072 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 17:04:52 +0100 Subject: [PATCH 269/430] Use utcnow from date util for http.ban (#43686) --- homeassistant/components/http/ban.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 2c02cc7391f..14d81a1eb6e 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -15,7 +15,7 @@ from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.util.yaml import dump +from homeassistant.util import dt as dt_util, yaml # mypy: allow-untyped-defs, no-check-untyped-defs @@ -179,7 +179,7 @@ class IpBan: def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None: """Initialize IP Ban object.""" self.ip_address = ip_address(ip_ban) - self.banned_at = banned_at or datetime.utcnow() + self.banned_at = banned_at or dt_util.utcnow() async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]: @@ -208,10 +208,6 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBa 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_ = { - str(ip_ban.ip_address): { - ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S") - } - } + ip_ = {str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()}} out.write("\n") - out.write(dump(ip_)) + out.write(yaml.dump(ip_)) From 5b6d9abe2a087531ed3ec9e09f3636ae902a64d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Nov 2020 17:48:43 +0100 Subject: [PATCH 270/430] Await callbacks to keep cleaner stacktraces (#43693) --- homeassistant/components/mqtt/__init__.py | 16 +++++++++------- homeassistant/helpers/debounce.py | 12 ++++++++---- homeassistant/helpers/discovery.py | 14 ++++++++------ tests/test_loader.py | 3 ++- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index bd89a2364be..800d9e4eb73 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1485,20 +1485,22 @@ def async_subscribe_connection_status(hass, connection_status_callback): connection_status_callback_job = HassJob(connection_status_callback) - @callback - def connected(): - hass.async_add_hass_job(connection_status_callback_job, True) + async def connected(): + task = hass.async_run_hass_job(connection_status_callback_job, True) + if task: + await task - @callback - def disconnected(): - _LOGGER.error("Calling connection_status_callback, False") - hass.async_add_hass_job(connection_status_callback_job, False) + async def disconnected(): + task = hass.async_run_hass_job(connection_status_callback_job, False) + if task: + await task subscriptions = { "connect": async_dispatcher_connect(hass, MQTT_CONNECTED, connected), "disconnect": async_dispatcher_connect(hass, MQTT_DISCONNECTED, disconnected), } + @callback def unsubscribe(): subscriptions["connect"]() subscriptions["disconnect"]() diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 988ea9f0051..23727c2a00f 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -48,7 +48,7 @@ class Debouncer: async def async_call(self) -> None: """Call the function.""" - assert self.function is not None + assert self._job is not None if self._timer_task: if not self._execute_at_end_of_timer: @@ -70,13 +70,15 @@ class Debouncer: if self._timer_task: return - await self.hass.async_add_hass_job(self._job) # type: ignore + task = self.hass.async_run_hass_job(self._job) + if task: + await task self._schedule_timer() async def _handle_timer_finish(self) -> None: """Handle a finished timer.""" - assert self.function is not None + assert self._job is not None self._timer_task = None @@ -95,7 +97,9 @@ class Debouncer: return # type: ignore try: - await self.hass.async_add_hass_job(self._job) # type: ignore + task = self.hass.async_run_hass_job(self._job) + if task: + await task except Exception: # pylint: disable=broad-except self.logger.exception("Unexpected exception from %s", self.function) diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index d41e8174bc9..acde8d73a50 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -44,13 +44,14 @@ def async_listen( job = core.HassJob(callback) - @core.callback - def discovery_event_listener(event: core.Event) -> None: + async 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_hass_job( + task = hass.async_run_hass_job( job, event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED) ) + if task: + await task hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener) @@ -114,8 +115,7 @@ def async_listen_platform( service = EVENT_LOAD_PLATFORM.format(component) job = core.HassJob(callback) - @core.callback - def discovery_platform_listener(event: core.Event) -> None: + async def discovery_platform_listener(event: core.Event) -> None: """Listen for platform discovery events.""" if event.data.get(ATTR_SERVICE) != service: return @@ -125,7 +125,9 @@ def async_listen_platform( if not platform: return - hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED)) + task = hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED)) + if task: + await task hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener) diff --git a/tests/test_loader.py b/tests/test_loader.py index 69e6688b9de..c05240893de 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,9 +1,9 @@ """Test to verify that we can load components.""" import pytest +from homeassistant import core, loader from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light -import homeassistant.loader as loader from tests.async_mock import ANY, patch from tests.common import MockModule, async_mock_service, mock_integration @@ -83,6 +83,7 @@ async def test_helpers_wrapper(hass): result = [] + @core.callback def discovery_callback(service, discovered): """Handle discovery callback.""" result.append(discovered) From 29091f4537847fa4287511c33c0a17c3d86b9175 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 27 Nov 2020 18:06:15 +0100 Subject: [PATCH 271/430] Update xknx to 0.15.6 (#43645) --- homeassistant/components/knx/__init__.py | 2 -- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 3dd6a97596d..1d547e895bf 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -326,8 +326,6 @@ class KNXModule: "knx_event", {"address": str(telegram.group_address), "data": telegram.payload.value}, ) - # False signals XKNX to proceed with processing telegrams. - return False async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6b253c2a010..631a6329c8c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.15.4"], + "requirements": ["xknx==0.15.6"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/requirements_all.txt b/requirements_all.txt index 0e745d9995f..c7b98eb2a70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2310,7 +2310,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.15.4 +xknx==0.15.6 # homeassistant.components.bluesound # homeassistant.components.rest From 0d5dc4aeee4685c6a763817c920a6768ea51a1cd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 27 Nov 2020 19:25:17 +0100 Subject: [PATCH 272/430] Add system health check to Spotify (#43249) --- .coveragerc | 1 + homeassistant/components/spotify/strings.json | 5 +++++ .../components/spotify/system_health.py | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 homeassistant/components/spotify/system_health.py diff --git a/.coveragerc b/.coveragerc index 064fe422358..9989f20aff3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -832,6 +832,7 @@ omit = homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py + homeassistant/components/spotify/system_health.py homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 8745df8e56f..74df79c4d78 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -16,5 +16,10 @@ "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, "create_entry": { "default": "Successfully authenticated with Spotify." } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API endpoint reachable" + } } } diff --git a/homeassistant/components/spotify/system_health.py b/homeassistant/components/spotify/system_health.py new file mode 100644 index 00000000000..a22f7b8a821 --- /dev/null +++ b/homeassistant/components/spotify/system_health.py @@ -0,0 +1,20 @@ +"""Provide info to system health.""" +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + return { + "api_endpoint_reachable": system_health.async_check_can_reach_url( + hass, "https://api.spotify.com" + ) + } From fd6a2b078b744b4018113702506a7262fe4d5f50 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 27 Nov 2020 13:09:37 -0700 Subject: [PATCH 273/430] Fix exception upon lock initialization on V2 SimpliSafe systems (#43705) --- homeassistant/components/simplisafe/lock.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 000b1cd9abb..08ffb82d24f 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -17,13 +17,17 @@ ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" 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(simplisafe, system, lock) - for system in simplisafe.systems.values() - for lock in system.locks.values() - ] - ) + locks = [] + + for system in simplisafe.systems.values(): + if system.version == 2: + LOGGER.info("Skipping lock setup for V2 system: %s", system.system_id) + continue + + for lock in system.locks.values(): + locks.append(SimpliSafeLock(simplisafe, system, lock)) + + async_add_entities(locks) class SimpliSafeLock(SimpliSafeEntity, LockEntity): From e10762af9b11938ede30b4e5fb3b50825a126ffa Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Fri, 27 Nov 2020 21:11:57 +0100 Subject: [PATCH 274/430] Bump RFLink to v0.0.55 (#43704) --- 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 266092581f0..cdcfe97c219 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,6 +2,6 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.54"], + "requirements": ["rflink==0.0.55"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c7b98eb2a70..840d2df684d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ restrictedpython==5.0 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.54 +rflink==0.0.55 # homeassistant.components.ring ring_doorbell==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c858aea7be3..35b1198b1e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -941,7 +941,7 @@ regenmaschine==3.0.0 restrictedpython==5.0 # homeassistant.components.rflink -rflink==0.0.54 +rflink==0.0.55 # homeassistant.components.ring ring_doorbell==0.6.0 From 7c0e148b59c35bab0f5b1e6db9e57d31ac520e22 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 27 Nov 2020 15:14:19 -0600 Subject: [PATCH 275/430] Proxy Plex media browser images (#43111) Co-authored-by: rajlaud <50647620+rajlaud@users.noreply.github.com> Co-authored-by: Paulus Schoutsen --- .../components/plex/media_browser.py | 132 +++++++++--------- homeassistant/components/plex/media_player.py | 17 ++- homeassistant/components/plex/server.py | 1 + 3 files changed, 85 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 56e6f68a968..cfc5a12d6c5 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -52,14 +52,69 @@ ITEM_TYPE_MEDIA_CLASS = { _LOGGER = logging.getLogger(__name__) -def browse_media( - entity_id, plex_server, media_content_type=None, media_content_id=None -): +def browse_media(entity, is_internal, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + def item_payload(item): + """Create response payload for a single media item.""" + try: + media_class = ITEM_TYPE_MEDIA_CLASS[item.type] + except KeyError as err: + _LOGGER.debug("Unknown type received: %s", item.type) + raise UnknownMediaType from err + payload = { + "title": item.title, + "media_class": media_class, + "media_content_id": str(item.ratingKey), + "media_content_type": item.type, + "can_play": True, + "can_expand": item.type in EXPANDABLES, + } + if hasattr(item, "thumbUrl"): + entity.plex_server.thumbnail_cache.setdefault( + str(item.ratingKey), item.thumbUrl + ) + + if is_internal: + thumbnail = item.thumbUrl + else: + thumbnail = entity.get_browse_image_url(item.type, item.ratingKey) + + payload["thumbnail"] = thumbnail + + return BrowseMedia(**payload) + + def library_payload(library_id): + """Create response payload to describe contents of a specific library.""" + library = entity.plex_server.library.sectionByID(library_id) + library_info = library_section_payload(library) + library_info.children = [] + library_info.children.append(special_library_payload(library_info, "On Deck")) + library_info.children.append( + special_library_payload(library_info, "Recently Added") + ) + for item in library.all(): + try: + library_info.children.append(item_payload(item)) + except UnknownMediaType: + continue + return library_info + + def playlists_payload(): + """Create response payload for all available playlists.""" + playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} + for playlist in entity.plex_server.playlists(): + try: + playlists_info["children"].append(item_payload(playlist)) + except UnknownMediaType: + continue + response = BrowseMedia(**playlists_info) + response.children_media_class = MEDIA_CLASS_PLAYLIST + return response + def build_item_response(payload): """Create response payload for the provided media query.""" - media = plex_server.lookup_media(**payload) + media = entity.plex_server.lookup_media(**payload) if media is None: return None @@ -85,19 +140,21 @@ def browse_media( if ( media_content_type and media_content_type == "server" - and media_content_id != plex_server.machine_identifier + and media_content_id != entity.plex_server.machine_identifier ): raise BrowseError( - f"Plex server with ID '{media_content_id}' is not associated with {entity_id}" + f"Plex server with ID '{media_content_id}' is not associated with {entity.entity_id}" ) if special_folder: if media_content_type == "server": - library_or_section = plex_server.library + library_or_section = entity.plex_server.library children_media_class = MEDIA_CLASS_DIRECTORY - title = plex_server.friendly_name + title = entity.plex_server.friendly_name elif media_content_type == "library": - library_or_section = plex_server.library.sectionByID(media_content_id) + library_or_section = entity.plex_server.library.sectionByID( + media_content_id + ) title = library_or_section.title try: children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE] @@ -133,10 +190,10 @@ def browse_media( try: if media_content_type in ["server", None]: - return server_payload(plex_server) + return server_payload(entity.plex_server) if media_content_type == "library": - return library_payload(plex_server, media_content_id) + return library_payload(media_content_id) except UnknownMediaType as err: raise BrowseError( @@ -144,7 +201,7 @@ def browse_media( ) from err if media_content_type == "playlists": - return playlists_payload(plex_server) + return playlists_payload() payload = { "media_type": DOMAIN, @@ -156,27 +213,6 @@ def browse_media( return response -def item_payload(item): - """Create response payload for a single media item.""" - try: - media_class = ITEM_TYPE_MEDIA_CLASS[item.type] - except KeyError as err: - _LOGGER.debug("Unknown type received: %s", item.type) - raise UnknownMediaType from err - payload = { - "title": item.title, - "media_class": media_class, - "media_content_id": str(item.ratingKey), - "media_content_type": item.type, - "can_play": True, - "can_expand": item.type in EXPANDABLES, - } - if hasattr(item, "thumbUrl"): - payload["thumbnail"] = item.thumbUrl - - return BrowseMedia(**payload) - - def library_section_payload(section): """Create response payload for a single library section.""" try: @@ -229,33 +265,3 @@ def server_payload(plex_server): server_info.children.append(library_section_payload(library)) server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD)) return server_info - - -def library_payload(plex_server, library_id): - """Create response payload to describe contents of a specific library.""" - library = plex_server.library.sectionByID(library_id) - library_info = library_section_payload(library) - library_info.children = [] - library_info.children.append(special_library_payload(library_info, "On Deck")) - library_info.children.append( - special_library_payload(library_info, "Recently Added") - ) - for item in library.all(): - try: - library_info.children.append(item_payload(item)) - except UnknownMediaType: - continue - return library_info - - -def playlists_payload(plex_server): - """Create response payload for all available playlists.""" - playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} - for playlist in plex_server.playlists(): - try: - playlists_info["children"].append(item_payload(playlist)) - except UnknownMediaType: - continue - response = BrowseMedia(**playlists_info) - response.children_media_class = MEDIA_CLASS_PLAYLIST - return response diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 5f72b912a70..295254aa612 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -26,6 +26,7 @@ from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYI from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.network import is_internal_request from homeassistant.util import dt as dt_util from .const import ( @@ -629,10 +630,22 @@ class PlexMediaPlayer(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + is_internal = is_internal_request(self.hass) return await self.hass.async_add_executor_job( browse_media, - self.entity_id, - self.plex_server, + self, + is_internal, media_content_type, media_content_id, ) + + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Get media image from Plex server.""" + image_url = self.plex_server.thumbnail_cache.get(media_content_id) + if image_url: + result = await self._async_fetch_image(image_url) + return result + + return (None, None) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index baa41cae87f..ed84b0c5d8f 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -97,6 +97,7 @@ class PlexServer: immediate=True, function=self._async_update_platforms, ).async_call + self.thumbnail_cache = {} # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_ID in server_config: From c42b65003135bb967a89ec4b7663457bfc5281f8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 27 Nov 2020 16:39:44 -0500 Subject: [PATCH 276/430] Bump up ZHA dependencies (#43707) bellows==0.21.0 zigpy==0.28.1 zha-quirks==0.0.47 --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 34fd5f1f461..780bb5bc999 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.20.3", + "bellows==0.21.0", "pyserial==3.4", - "zha-quirks==0.0.46", + "zha-quirks==0.0.47", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.28.0", + "zigpy==0.28.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.2.2" diff --git a/requirements_all.txt b/requirements_all.txt index 840d2df684d..604159dcf14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ beautifulsoup4==4.9.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.20.3 +bellows==0.21.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.13 @@ -2344,7 +2344,7 @@ zengge==0.2 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.46 +zha-quirks==0.0.47 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2368,7 +2368,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.2.2 # homeassistant.components.zha -zigpy==0.28.0 +zigpy==0.28.1 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35b1198b1e0..5be4307b9ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.20.3 +bellows==0.21.0 # homeassistant.components.blebox blebox_uniapi==1.3.2 @@ -1134,7 +1134,7 @@ zeep[async]==4.0.0 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.46 +zha-quirks==0.0.47 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1152,4 +1152,4 @@ zigpy-zigate==0.7.3 zigpy-znp==0.2.2 # homeassistant.components.zha -zigpy==0.28.0 +zigpy==0.28.1 From d5c2ef5a949759dffee13eafe7c88b48db546994 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 28 Nov 2020 00:03:36 +0000 Subject: [PATCH 277/430] [ci skip] Translation update --- .../components/abode/translations/ru.json | 17 ++++++++++- .../abode/translations/zh-Hant.json | 17 ++++++++++- .../fireservicerota/translations/es.json | 29 +++++++++++++++++++ .../components/spotify/translations/en.json | 5 ++++ .../components/spotify/translations/ru.json | 5 ++++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/fireservicerota/translations/es.json diff --git a/homeassistant/components/abode/translations/ru.json b/homeassistant/components/abode/translations/ru.json index 2a92a44e1b7..04efaa6e519 100644 --- a/homeassistant/components/abode/translations/ru.json +++ b/homeassistant/components/abode/translations/ru.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA." }, "step": { + "mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)" + }, + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode" + }, + "reauth_confirm": { + "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" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json index b23eb9d3707..d3e1db007f5 100644 --- a/homeassistant/components/abode/translations/zh-Hant.json +++ b/homeassistant/components/abode/translations/zh-Hant.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\u7121\u6548" }, "step": { + "mfa": { + "data": { + "mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\uff086 \u4f4d\uff09" + }, + "title": "\u8f38\u5165 Abode \u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc" + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json new file mode 100644 index 00000000000..9f27181dfe5 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "reauth": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Los tokens de autenticaci\u00f3n ya no son v\u00e1lidos, inicia sesi\u00f3n para recrearlos." + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "Sitio web", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index 1c04a5868bc..73ea219105b 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -18,5 +18,10 @@ "title": "Reauthenticate Integration" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API endpoint reachable" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index c9451ad76f3..722cb125169 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -18,5 +18,10 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API Spotify" + } } } \ No newline at end of file From 65bc128c86db4f5f47b87bc28248d9a9a0a9aa57 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 28 Nov 2020 09:29:16 +0100 Subject: [PATCH 278/430] Bump pyatmo to v4.2.1 (#43713) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 824b835b01a..aecd119454a 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==4.2.0" + "pyatmo==4.2.1" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 604159dcf14..197d1bc1aa8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1277,7 +1277,7 @@ pyarlo==0.2.3 pyatag==0.3.4.4 # homeassistant.components.netatmo -pyatmo==4.2.0 +pyatmo==4.2.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5be4307b9ca..1ff2ab833f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -646,7 +646,7 @@ pyarlo==0.2.3 pyatag==0.3.4.4 # homeassistant.components.netatmo -pyatmo==4.2.0 +pyatmo==4.2.1 # homeassistant.components.blackbird pyblackbird==0.5 From 6fa3e287da62c59894d53de2aaed6be33c640584 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 28 Nov 2020 13:11:13 +0100 Subject: [PATCH 279/430] Add support to control cooling in deCONZ climate platform (#43720) * Add fan support * Add HVAC cool support * Fix Martins comment from #43607 * Add preset support * Improve climate test coverage * Remove fan support * Remove preset support * Remove last preset piece --- homeassistant/components/deconz/climate.py | 54 +++++++++---- homeassistant/components/deconz/cover.py | 1 + tests/components/deconz/test_climate.py | 90 +++++++++++++++++++++- 3 files changed, 128 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 4a6f2bd4937..afa9e8dcd9f 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -4,6 +4,7 @@ from pydeconz.sensor import Thermostat from homeassistant.components.climate import DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, @@ -16,7 +17,12 @@ from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -HVAC_MODES = {HVAC_MODE_AUTO: "auto", HVAC_MODE_HEAT: "heat", HVAC_MODE_OFF: "off"} +HVAC_MODES = { + HVAC_MODE_AUTO: "auto", + HVAC_MODE_COOL: "cool", + HVAC_MODE_HEAT: "heat", + HVAC_MODE_OFF: "off", +} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -61,10 +67,22 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): TYPE = DOMAIN + def __init__(self, device, gateway): + """Set up thermostat device.""" + super().__init__(device, gateway) + + self._hvac_modes = dict(HVAC_MODES) + if "coolsetpoint" not in device.raw["config"]: + self._hvac_modes.pop(HVAC_MODE_COOL) + + self._features = SUPPORT_TARGET_TEMPERATURE + @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE + return self._features + + # HVAC control @property def hvac_mode(self): @@ -72,7 +90,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): Need to be one of HVAC_MODE_*. """ - for hass_hvac_mode, device_mode in HVAC_MODES.items(): + for hass_hvac_mode, device_mode in self._hvac_modes.items(): if self._device.mode == device_mode: return hass_hvac_mode @@ -84,16 +102,29 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def hvac_modes(self) -> list: """Return the list of available hvac operation modes.""" - return list(HVAC_MODES) + return list(self._hvac_modes) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode not in self._hvac_modes: + raise ValueError(f"Unsupported HVAC mode {hvac_mode}") + + data = {"mode": self._hvac_modes[hvac_mode]} + + await self._device.async_set_config(data) + + # Temperature control @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._device.temperature @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the target temperature.""" + if self._device.mode == "cool": + return self._device.coolsetpoint return self._device.heatsetpoint async def async_set_temperature(self, **kwargs): @@ -102,15 +133,8 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") data = {"heatsetpoint": kwargs[ATTR_TEMPERATURE] * 100} - - await self._device.async_set_config(data) - - async def async_set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - if hvac_mode not in HVAC_MODES: - raise ValueError(f"Unsupported mode {hvac_mode}") - - data = {"mode": HVAC_MODES[hvac_mode]} + if self._device.mode == "cool": + data = {"coolsetpoint": kwargs[ATTR_TEMPERATURE] * 100} await self._device.async_set_config(data) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 48218cf893a..c8d9a16d6c8 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -116,6 +116,7 @@ class DeconzCover(DeconzDevice, CoverEntity): """Return the current tilt position of the cover.""" if self._device.tilt is not None: return 100 - self._device.tilt + return None async def async_set_cover_tilt_position(self, **kwargs): """Tilt the cover to a specific position.""" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 5d2e6d614a3..751f1572239 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -73,7 +73,7 @@ async def test_no_sensors(hass): assert len(hass.states.async_all()) == 0 -async def test_climate_devices(hass): +async def test_climate_device_without_cooling_support(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) @@ -81,7 +81,15 @@ async def test_climate_devices(hass): gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 - assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO + climate_thermostat = hass.states.get("climate.thermostat") + assert climate_thermostat.state == HVAC_MODE_AUTO + assert climate_thermostat.attributes["hvac_modes"] == [ + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert climate_thermostat.attributes["current_temperature"] == 22.6 + assert climate_thermostat.attributes["temperature"] == 22.0 assert hass.states.get("sensor.thermostat") is None assert hass.states.get("sensor.thermostat_battery_level").state == "100" assert hass.states.get("climate.presence_sensor") is None @@ -221,6 +229,84 @@ async def test_climate_devices(hass): assert len(hass.states.async_all()) == 0 +async def test_climate_device_with_cooling_support(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": None, + "heatsetpoint": 2222, + "mode": "heat", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 2 + climate_thermostat = hass.states.get("climate.zen_01") + assert climate_thermostat.state == HVAC_MODE_HEAT + assert climate_thermostat.attributes["hvac_modes"] == [ + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert climate_thermostat.attributes["current_temperature"] == 23.2 + assert climate_thermostat.attributes["temperature"] == 22.2 + assert hass.states.get("sensor.zen_01_battery_level").state == "25" + + # Event signals thermostat state cool + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"mode": "cool"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").state == HVAC_MODE_COOL + + # Verify service calls + + thermostat_device = gateway.api.sensors["0"] + + # Service set temperature to 20 + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_TEMPERATURE: 20}, + blocking=True, + ) + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"coolsetpoint": 2000.0} + ) + + async def test_clip_climate_device(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) From cb96bd9d0b46899cac433fbd32002d9ce4f95739 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Nov 2020 13:19:58 +0100 Subject: [PATCH 280/430] Blueprint config to override blueprint (#43724) --- homeassistant/components/blueprint/models.py | 2 +- tests/components/blueprint/test_models.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 6e79b6da842..73417722dcc 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -172,7 +172,7 @@ class BlueprintInputs: processed = placeholder.substitute( self.blueprint.data, self.inputs_with_default ) - combined = {**self.config_with_inputs, **processed} + combined = {**processed, **self.config_with_inputs} # From config_with_inputs combined.pop(CONF_USE_BLUEPRINT) # From blueprint diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index f5d94a9301a..3680889e56b 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -133,15 +133,24 @@ def test_blueprint_validate(): ) -def test_blueprint_inputs(blueprint_1): +def test_blueprint_inputs(blueprint_2): """Test blueprint inputs.""" inputs = models.BlueprintInputs( - blueprint_1, - {"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}}, + blueprint_2, + { + "use_blueprint": { + "path": "bla", + "input": {"test-placeholder": 1, "test-placeholder-default": 12}, + }, + "example-default": {"overridden": "via-config"}, + }, ) inputs.validate() - assert inputs.inputs == {"test-placeholder": 1} - assert inputs.async_substitute() == {"example": 1} + assert inputs.inputs == {"test-placeholder": 1, "test-placeholder-default": 12} + assert inputs.async_substitute() == { + "example": 1, + "example-default": {"overridden": "via-config"}, + } def test_blueprint_inputs_validation(blueprint_1): From 337b8d279eabc4e2eb52e5b449ab89d773de6b21 Mon Sep 17 00:00:00 2001 From: moinmoin-sh <53935853+moinmoin-sh@users.noreply.github.com> Date: Sat, 28 Nov 2020 19:42:29 +0100 Subject: [PATCH 281/430] Ensure MariaDB/MySQL can be purged and handle states being deleted out from under the recorder (#43610) * MariaDB doesn't purge #42402 This addresses home-assistant#42402 Relationships within table "states" and between tables "states" and "events " home-assistant#40467 prevent the purge from working correctly. The database increases w/o any purge. This proposal sets related indices to NULL and permits deleting of rows. Further explanations can be found here home-assistant#42402 This proposal also allows to purge the tables "events" and "states" in any order. * Update models.py Corrected for Black style requirements * Update homeassistant/components/recorder/models.py Co-authored-by: J. Nick Koston * Add the options to foreign key constraints * purge old states when database gets deleted out from under us * pylint Co-authored-by: J. Nick Koston --- homeassistant/components/recorder/__init__.py | 8 ++++ .../components/recorder/migration.py | 40 ++++++++++++++++++- homeassistant/components/recorder/models.py | 12 ++++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 18d364315b7..0f8a5ae7f8f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -514,6 +514,14 @@ class Recorder(threading.Thread): self.event_session.expunge(dbstate) self._pending_expunge = [] self.event_session.commit() + except exc.IntegrityError as err: + _LOGGER.error( + "Integrity error executing query (database likely deleted out from under us): %s", + err, + ) + self.event_session.rollback() + self._old_states = {} + raise except Exception as err: _LOGGER.error("Error executing query: %s", err) self.event_session.rollback() diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e88852e4a5a..c633c114b46 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,12 +1,13 @@ """Schema migration helpers.""" import logging -from sqlalchemy import Table, text +from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text from sqlalchemy.engine import reflection from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError +from sqlalchemy.schema import AddConstraint, DropConstraint from .const import DOMAIN -from .models import SCHEMA_VERSION, Base, SchemaChanges +from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -205,6 +206,39 @@ def _add_columns(engine, table_name, columns_def): ) +def _update_states_table_with_foreign_key_options(engine): + """Add the options to foreign key constraints.""" + inspector = reflection.Inspector.from_engine(engine) + alters = [] + for foreign_key in inspector.get_foreign_keys(TABLE_STATES): + if foreign_key["name"] and not foreign_key["options"]: + alters.append( + { + "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]), + "columns": foreign_key["constrained_columns"], + } + ) + + if not alters: + return + + states_key_constraints = Base.metadata.tables[TABLE_STATES].foreign_key_constraints + old_states_table = Table( # noqa: F841 pylint: disable=unused-variable + TABLE_STATES, MetaData(), *[alter["old_fk"] for alter in alters] + ) + + for alter in alters: + try: + engine.execute(DropConstraint(alter["old_fk"])) + for fkc in states_key_constraints: + if fkc.column_keys == alter["columns"]: + engine.execute(AddConstraint(fkc)) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not update foreign options in %s table", TABLE_STATES + ) + + def _apply_update(engine, new_version, old_version): """Perform operations to bring schema up to date.""" if new_version == 1: @@ -277,6 +311,8 @@ def _apply_update(engine, new_version, old_version): _drop_index(engine, "states", "ix_states_entity_id") _create_index(engine, "events", "ix_events_event_type_time_fired") _drop_index(engine, "events", "ix_events_event_type") + elif new_version == 10: + _update_states_table_with_foreign_key_options(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 4756ac13ce3..6c8b6050a9a 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 9 +SCHEMA_VERSION = 10 _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ TABLE_STATES = "states" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" -ALL_TABLES = [TABLE_EVENTS, TABLE_STATES, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] +ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] class Events(Base): # type: ignore @@ -102,11 +102,15 @@ class States(Base): # type: ignore entity_id = Column(String(255)) state = Column(String(255)) attributes = Column(Text) - event_id = Column(Integer, ForeignKey("events.event_id"), index=True) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow) last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True) created = Column(DateTime(timezone=True), default=dt_util.utcnow) - old_state_id = Column(Integer, ForeignKey("states.state_id")) + old_state_id = Column( + Integer, ForeignKey("states.state_id", ondelete="SET NULL"), index=True + ) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) From a4ae2d210ee0f7f431da5845afeef056041589b5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 28 Nov 2020 21:17:16 +0100 Subject: [PATCH 282/430] Bump Brother library to version 0.1.20 (#43628) --- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/brother/sensor.py | 6 +----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/brother/test_sensor.py | 6 ++---- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index bc63de34b2a..0e534147cb1 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.18"], + "requirements": ["brother==0.1.20"], "zeroconf": [{"type": "_printer._tcp.local.", "name":"Brother*"}], "config_flow": true, "quality_scale": "platinum" diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 97890e83cff..40e2deae67d 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,9 +1,6 @@ """Support for the Brother service.""" -from datetime import timedelta - from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.dt import utcnow from .const import ( ATTR_BLACK_DRUM_COUNTER, @@ -79,8 +76,7 @@ class BrotherPrinterSensor(CoordinatorEntity): def state(self): """Return the state.""" if self.kind == ATTR_UPTIME: - uptime = utcnow() - timedelta(seconds=self.coordinator.data.get(self.kind)) - return uptime.replace(microsecond=0).isoformat() + return self.coordinator.data.get(self.kind).isoformat() return self.coordinator.data.get(self.kind) @property diff --git a/requirements_all.txt b/requirements_all.txt index 197d1bc1aa8..c93bdd10b47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ bravia-tv==1.0.8 broadlink==0.16.0 # homeassistant.components.brother -brother==0.1.18 +brother==0.1.20 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ff2ab833f4..41e73aa3db3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -207,7 +207,7 @@ bravia-tv==1.0.8 broadlink==0.16.0 # homeassistant.components.brother -brother==0.1.18 +brother==0.1.20 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index ac32e1b983c..3f9ee9394b7 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow -from tests.async_mock import patch +from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed, load_fixture from tests.components.brother import init_integration @@ -39,9 +39,7 @@ async def test_sensors(hass): disabled_by=None, ) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) - with patch( - "homeassistant.components.brother.sensor.utcnow", return_value=test_time - ), patch( + with patch("brother.datetime", utcnow=Mock(return_value=test_time)), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("brother_printer_data.json")), ): From 5d7d2dee2ab45cdd9797422da40a5dba8daa10a8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 28 Nov 2020 21:18:06 +0100 Subject: [PATCH 283/430] Bump hatasmota to 0.1.2 (#43719) --- homeassistant/components/tasmota/light.py | 4 +- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 71 +++++++++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index c2e145600f8..efab8dcaae3 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -92,7 +92,6 @@ class TasmotaLight( if light_type != LIGHT_TYPE_NONE: supported_features |= SUPPORT_BRIGHTNESS - supported_features |= SUPPORT_TRANSITION if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]: supported_features |= SUPPORT_COLOR_TEMP @@ -104,6 +103,9 @@ class TasmotaLight( if light_type in [LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: supported_features |= SUPPORT_WHITE_VALUE + if self._tasmota_entity.supports_transition: + supported_features |= SUPPORT_TRANSITION + self._supported_features = supported_features @callback diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 076371c9792..a4d6ec6036f 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.1"], + "requirements": ["hatasmota==0.1.2"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index c93bdd10b47..4cbbc82748b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.1 +hatasmota==0.1.2 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41e73aa3db3..a28cd56d3de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.1.1 +hatasmota==0.1.2 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 7bfa4ac6d99..627eb5198aa 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -62,6 +62,30 @@ async def test_attributes_on_off(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("supported_features") == 0 +async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (dimmer) + config["ty"] = 1 # Tuya device + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.attributes.get("effect_list") is None + assert state.attributes.get("min_mireds") is None + assert state.attributes.get("max_mireds") is None + assert state.attributes.get("supported_features") == SUPPORT_BRIGHTNESS + + async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -509,6 +533,53 @@ async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() +async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["ty"] = 1 # Tuya device + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT message is sent + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn the light off and verify MQTT message is sent + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT messages are sent + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer3 75", 0, False + ) + + async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) From 898a07fa75c010b29aecc8a35c6190d5564dd536 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Nov 2020 21:19:10 +0100 Subject: [PATCH 284/430] Make threshold binary sensor faster (#43695) --- homeassistant/components/threshold/binary_sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 55ac21f6f13..fa05fc09687 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -71,7 +71,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass, entity_id, name, lower, upper, hysteresis, device_class ) ], - True, ) @@ -88,8 +87,8 @@ class ThresholdSensor(BinarySensorEntity): self._hysteresis = hysteresis self._device_class = device_class - self._state_position = None - self._state = False + self._state_position = POSITION_UNKNOWN + self._state = None self.sensor_value = None @callback @@ -107,7 +106,8 @@ class ThresholdSensor(BinarySensorEntity): self.sensor_value = None _LOGGER.warning("State is not numerical") - hass.async_add_job(self.async_update_ha_state, True) + self._update_state() + self.async_write_ha_state() async_track_state_change_event( hass, [entity_id], async_threshold_sensor_state_listener @@ -156,8 +156,9 @@ class ThresholdSensor(BinarySensorEntity): ATTR_UPPER: self._threshold_upper, } - async def async_update(self): - """Get the latest data and updates the states.""" + @callback + def _update_state(self): + """Update the state.""" def below(threshold): """Determine if the sensor value is below a threshold.""" From 2c6a72b3ad0b54b384952807fd6fa8d91bd22738 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 28 Nov 2020 21:42:51 +0100 Subject: [PATCH 285/430] Add Response switch platform to FireServiceRota integration (#43700) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + .../components/fireservicerota/__init__.py | 19 ++- .../fireservicerota/binary_sensor.py | 24 +-- .../components/fireservicerota/config_flow.py | 3 +- .../components/fireservicerota/sensor.py | 8 +- .../components/fireservicerota/switch.py | 154 ++++++++++++++++++ 6 files changed, 189 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/fireservicerota/switch.py diff --git a/.coveragerc b/.coveragerc index 9989f20aff3..24d6d3fe651 100644 --- a/.coveragerc +++ b/.coveragerc @@ -266,6 +266,7 @@ omit = homeassistant/components/fireservicerota/binary_sensor.py homeassistant/components/fireservicerota/const.py homeassistant/components/fireservicerota/sensor.py + homeassistant/components/fireservicerota/switch.py homeassistant/components/firmata/__init__.py homeassistant/components/firmata/binary_sensor.py homeassistant/components/firmata/board.py diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 2eb29a95cf6..35bd71cf794 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -13,6 +13,7 @@ from pyfireservicerota import ( from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -25,7 +26,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -SUPPORTED_PLATFORMS = {SENSOR_DOMAIN, BINARYSENSOR_DOMAIN} +SUPPORTED_PLATFORMS = {SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN} async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -191,6 +192,7 @@ class FireServiceRotaClient: self.token_refresh_failure = False self.incident_id = None + self.on_duty = False self.fsr = FireServiceRota(base_url=self._url, token_info=self._tokens) @@ -229,24 +231,29 @@ class FireServiceRotaClient: self.fsr.get_availability, str(self._hass.config.time_zone) ) + self.on_duty = bool(data.get("available")) + _LOGGER.debug("Updated availability data: %s", data) return data async def async_response_update(self) -> object: """Get the latest incident response data.""" - data = self.websocket.incident_data() - if data is None or "id" not in data: + + if not self.incident_id: return - self.incident_id = data("id") - _LOGGER.debug("Updating incident response data for id: %s", self.incident_id) + _LOGGER.debug("Updating response data for incident id %s", self.incident_id) return await self.update_call(self.fsr.get_incident_response, self.incident_id) async def async_set_response(self, value) -> None: """Set incident response status.""" + + if not self.incident_id: + return + _LOGGER.debug( - "Setting incident response for incident '%s' to status '%s'", + "Setting incident response for incident id '%s' to state '%s'", self.incident_id, value, ) diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index bef6ebe3f8d..afbabcd98a3 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -19,19 +19,22 @@ async def async_setup_entry( ) -> None: """Set up FireServiceRota binary sensor based on a config entry.""" + client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] + coordinator: DataUpdateCoordinator = hass.data[FIRESERVICEROTA_DOMAIN][ entry.entry_id ][DATA_COORDINATOR] - async_add_entities([ResponseBinarySensor(coordinator, entry)]) + async_add_entities([ResponseBinarySensor(coordinator, client, entry)]) class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of an FireServiceRota sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, entry): + def __init__(self, coordinator: DataUpdateCoordinator, client, entry): """Initialize.""" super().__init__(coordinator) + self._client = client self._unique_id = f"{entry.unique_id}_Duty" self._state = None @@ -44,7 +47,10 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): @property def icon(self) -> str: """Return the icon to use in the frontend.""" - return "mdi:calendar" + if self._state: + return "mdi:calendar-check" + + return "mdi:calendar-remove" @property def unique_id(self) -> str: @@ -52,16 +58,10 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): return self._unique_id @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the binary sensor.""" - if not self.coordinator.data: - return - data = self.coordinator.data - if "available" in data and data["available"]: - self._state = True - else: - self._state = False + self._state = self._client.on_duty _LOGGER.debug("Set state of entity 'Duty Binary Sensor' to '%s'", self._state) return self._state diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index e5c49fda6ad..f815566c316 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -5,7 +5,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME -from .const import DOMAIN, URL_LIST # pylint: disable=unused-import +# pylint: disable=relative-beyond-top-level +from .const import DOMAIN, URL_LIST DATA_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index cba05f4fa5e..cb0ae761975 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType +# pylint: disable=relative-beyond-top-level from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -73,6 +74,7 @@ class IncidentsSensor(RestoreEntity): return attr for value in ( + "id", "trigger", "created_at", "message_to_speech_url", @@ -106,7 +108,9 @@ class IncidentsSensor(RestoreEntity): if state: self._state = state.state self._state_attributes = state.attributes - _LOGGER.debug("Restored entity 'Incidents' state to: %s", self._state) + if "id" in self._state_attributes: + self._client.incident_id = self._state_attributes["id"] + _LOGGER.debug("Restored entity 'Incidents' to: %s", self._state) self.async_on_remove( async_dispatcher_connect( @@ -125,4 +129,6 @@ class IncidentsSensor(RestoreEntity): self._state = data["body"] self._state_attributes = data + if "id" in self._state_attributes: + self._client.incident_id = self._state_attributes["id"] self.async_write_ha_state() diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py new file mode 100644 index 00000000000..f7a6a13db2b --- /dev/null +++ b/homeassistant/components/fireservicerota/switch.py @@ -0,0 +1,154 @@ +"""Switch platform for FireServiceRota integration.""" +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType + +# pylint: disable=relative-beyond-top-level +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up FireServiceRota switch based on a config entry.""" + client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] + + coordinator = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_COORDINATOR] + + async_add_entities([ResponseSwitch(coordinator, client, entry)]) + + +class ResponseSwitch(SwitchEntity): + """Representation of an FireServiceRota switch.""" + + def __init__(self, coordinator, client, entry): + """Initialize.""" + self._coordinator = coordinator + self._client = client + self._unique_id = f"{entry.unique_id}_Response" + self._entry_id = entry.entry_id + + self._state = None + self._state_attributes = {} + self._state_icon = None + + @property + def name(self) -> str: + """Return the name of the switch.""" + return "Incident Response" + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + if self._state_icon == "acknowledged": + return "mdi:run-fast" + if self._state_icon == "rejected": + return "mdi:account-off-outline" + + return "mdi:forum" + + @property + def is_on(self) -> bool: + """Get the assumed state of the switch.""" + return self._state + + @property + def unique_id(self) -> str: + """Return the unique ID for this switch.""" + return self._unique_id + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def available(self): + """Return if switch is available.""" + return self._client.on_duty + + @property + def device_state_attributes(self) -> object: + """Return available attributes for switch.""" + attr = {} + if not self._state_attributes: + return attr + + data = self._state_attributes + attr = { + key: data[key] + for key in ( + "user_name", + "assigned_skill_ids", + "responded_at", + "start_time", + "status", + "reported_status", + "arrived_at_station", + "available_at_incident_creation", + "active_duty_function_ids", + ) + if key in data + } + + _LOGGER.debug("Set attributes of entity 'Response Switch' to '%s'", attr) + return attr + + async def async_turn_on(self, **kwargs) -> None: + """Send Acknowlegde response status.""" + await self.async_set_response(True) + + async def async_turn_off(self, **kwargs) -> None: + """Send Reject response status.""" + await self.async_set_response(False) + + async def async_set_response(self, value) -> None: + """Send response status.""" + if not self._client.on_duty: + _LOGGER.debug( + "Cannot send incident response when not on duty", + ) + return + + await self._client.async_set_response(value) + self.client_update() + + async def async_added_to_hass(self) -> None: + """Register update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update", + self.client_update, + ) + ) + self.async_on_remove(self._coordinator.async_add_listener(self.on_duty_update)) + + @callback + def on_duty_update(self): + """Trigger on a on duty update.""" + self.async_write_ha_state() + + @callback + def client_update(self) -> None: + """Handle updated incident data from the client.""" + self.async_schedule_update_ha_state(True) + + async def async_update(self) -> bool: + """Update FireServiceRota response data.""" + data = await self._client.async_response_update() + + if not data or "status" not in data: + return + + self._state = data["status"] == "acknowledged" + self._state_attributes = data + self._state_icon = data["status"] + + _LOGGER.debug("Set state of entity 'Response Switch' to '%s'", self._state) From a2e1efca336163e74b7bb0ad27df7b039a012236 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 28 Nov 2020 17:29:56 -0500 Subject: [PATCH 286/430] Add additional events to enhance the ZHA device pairing experience (#43729) * support better feedback in the device pairing UI * update pairing events --- homeassistant/components/zha/core/const.py | 3 ++ homeassistant/components/zha/core/gateway.py | 55 ++++++++++++++++---- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 22f8f0f261d..12d928e172a 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -155,6 +155,9 @@ DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] DEFAULT_RADIO_TYPE = "ezsp" DEFAULT_BAUDRATE = 57600 DEFAULT_DATABASE_NAME = "zigbee.db" + +DEVICE_PAIRING_STATUS = "pairing_status" + DISCOVERY_KEY = "zha_discovery_info" DOMAIN = "zha" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 13a87c13226..bec429cdf22 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -3,6 +3,7 @@ import asyncio import collections from datetime import timedelta +from enum import Enum import itertools import logging import os @@ -54,6 +55,7 @@ from .const import ( DEBUG_LEVELS, DEBUG_RELAY_LOGGERS, DEFAULT_DATABASE_NAME, + DEVICE_PAIRING_STATUS, DOMAIN, SIGNAL_ADD_ENTITIES, SIGNAL_GROUP_MEMBERSHIP_CHANGE, @@ -94,6 +96,15 @@ EntityReference = collections.namedtuple( ) +class DevicePairingStatus(Enum): + """Status of a device.""" + + PAIRED = 1 + INTERVIEW_COMPLETE = 2 + CONFIGURED = 3 + INITIALIZED = 4 + + class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" @@ -240,8 +251,11 @@ class ZHAGateway: ZHA_GW_MSG, { ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED, - ATTR_NWK: device.nwk, - ATTR_IEEE: str(device.ieee), + ZHA_GW_MSG_DEVICE_INFO: { + ATTR_NWK: device.nwk, + ATTR_IEEE: str(device.ieee), + DEVICE_PAIRING_STATUS: DevicePairingStatus.PAIRED.name, + }, }, ) @@ -253,11 +267,14 @@ class ZHAGateway: ZHA_GW_MSG, { ATTR_TYPE: ZHA_GW_MSG_RAW_INIT, - ATTR_NWK: device.nwk, - ATTR_IEEE: str(device.ieee), - ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL, - ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, - ATTR_SIGNATURE: device.get_signature(), + ZHA_GW_MSG_DEVICE_INFO: { + ATTR_NWK: device.nwk, + ATTR_IEEE: str(device.ieee), + DEVICE_PAIRING_STATUS: DevicePairingStatus.INTERVIEW_COMPLETE.name, + ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL, + ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, + ATTR_SIGNATURE: device.get_signature(), + }, }, ) @@ -552,8 +569,8 @@ class ZHAGateway: ) await self._async_device_joined(zha_device) - device_info = zha_device.zha_device_info - + device_info = zha_device.device_info + device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.INITIALIZED.name async_dispatcher_send( self._hass, ZHA_GW_MSG, @@ -565,7 +582,17 @@ class ZHAGateway: async def _async_device_joined(self, zha_device: zha_typing.ZhaDeviceType) -> None: zha_device.available = True + device_info = zha_device.device_info await zha_device.async_configure() + device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name + async_dispatcher_send( + self._hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, + ) await zha_device.async_initialize(from_cache=False) async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) @@ -577,6 +604,16 @@ class ZHAGateway: ) # 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() + device_info = zha_device.device_info + device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name + async_dispatcher_send( + self._hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, + ) # force async_initialize() to fire so don't explicitly call it zha_device.available = False zha_device.update_available(True) From dd513147a511a4d0bf108eeb38c9a2b49b408622 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Nov 2020 23:33:32 +0100 Subject: [PATCH 287/430] Add a service target (#43725) --- .../automation/blueprints/motion_light.yaml | 11 ++++++----- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 12 +++++++----- homeassistant/helpers/selector.py | 10 ++++++++++ homeassistant/helpers/service.py | 5 +++++ tests/components/automation/test_blueprint.py | 2 +- tests/helpers/test_selector.py | 13 +++++++++++++ tests/helpers/test_service.py | 17 ++++++++++++----- 8 files changed, 55 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index aa787e3b2b5..c923778156d 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -9,11 +9,12 @@ blueprint: entity: domain: binary_sensor device_class: motion - light_entity: + light_target: name: Light selector: - entity: - domain: light + target: + entity: + domain: light # If motion is detected within the 120s delay, # we restart the script. @@ -28,7 +29,7 @@ trigger: action: - service: homeassistant.turn_on - entity_id: !placeholder light_entity + target: !placeholder light_target - wait_for_trigger: platform: state entity_id: !placeholder motion_entity @@ -36,4 +37,4 @@ action: to: "off" - delay: 120 - service: homeassistant.turn_off - entity_id: !placeholder light_entity + target: !placeholder light_target diff --git a/homeassistant/const.py b/homeassistant/const.py index 8917aa003a4..469c0fa7fbb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -170,6 +170,7 @@ CONF_STATE = "state" CONF_STATE_TEMPLATE = "state_template" CONF_STRUCTURE = "structure" CONF_SWITCHES = "switches" +CONF_TARGET = "target" CONF_TEMPERATURE_UNIT = "temperature_unit" CONF_TIMEOUT = "timeout" CONF_TIME_ZONE = "time_zone" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index af42b6373ba..37d7f312d94 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -62,6 +62,7 @@ from homeassistant.const import ( CONF_SERVICE, CONF_SERVICE_TEMPLATE, CONF_STATE, + CONF_TARGET, CONF_TIMEOUT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, @@ -881,7 +882,10 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -ENTITY_SERVICE_FIELDS = (ATTR_ENTITY_ID, ATTR_AREA_ID) +ENTITY_SERVICE_FIELDS = { + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_AREA_ID): vol.Any(ENTITY_MATCH_NONE, vol.All(ensure_list, [str])), +} def make_entity_service_schema( @@ -892,10 +896,7 @@ def make_entity_service_schema( vol.Schema( { **schema, - vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) - ), + **ENTITY_SERVICE_FIELDS, }, extra=extra, ), @@ -942,6 +943,7 @@ SERVICE_SCHEMA = vol.All( vol.Optional("data"): vol.All(dict, template_complex), vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, + vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS, } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 7ddd751753e..d81fa396c0b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -111,3 +111,13 @@ class TimeSelector(Selector): """Selector of a time value.""" CONFIG_SCHEMA = vol.Schema({}) + + +@SELECTORS.register("target") +class TargetSelector(Selector): + """Selector of a target value (area ID, device ID, entity ID etc). + + Value should follow cv.ENTITY_SERVICE_FIELDS format. + """ + + CONFIG_SCHEMA = vol.Schema({"entity": {"domain": str}}) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index c3700581b40..7aa4ac8b013 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_TEMPLATE, + CONF_TARGET, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, ) @@ -136,6 +137,10 @@ def async_prepare_call_from_config( domain, service = domain_service.split(".", 1) service_data = {} + + if CONF_TARGET in config: + service_data.update(config[CONF_TARGET]) + for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: if conf not in config: continue diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index a601998a00d..a455d1de5b5 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -123,7 +123,7 @@ async def test_motion_light(hass): "use_blueprint": { "path": "motion_light.yaml", "input": { - "light_entity": "light.kitchen", + "light_target": {"entity_id": "light.kitchen"}, "motion_entity": "binary_sensor.kitchen", }, } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 9d2d57cd6d0..2c2034147a1 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -117,3 +117,16 @@ def test_boolean_selector_schema(schema): def test_time_selector_schema(schema): """Test time selector.""" selector.validate_selector({"time": schema}) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"entity": {}}, + {"entity": {"domain": "light"}}, + ), +) +def test_target_selector_schema(schema): + """Test entity selector.""" + selector.validate_selector({"target": schema}) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 6f2cd4ba130..93125fba96d 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -175,18 +175,25 @@ class TestServiceHelpers(unittest.TestCase): "entity_id": "hello.world", "data": { "hello": "{{ 'goodbye' }}", - "data": {"value": "{{ 'complex' }}", "simple": "simple"}, + "effect": {"value": "{{ 'complex' }}", "simple": "simple"}, }, "data_template": {"list": ["{{ 'list' }}", "2"]}, + "target": {"area_id": "test-area-id", "entity_id": "will.be_overridden"}, } service.call_from_config(self.hass, config) self.hass.block_till_done() - assert self.calls[0].data["hello"] == "goodbye" - assert self.calls[0].data["data"]["value"] == "complex" - assert self.calls[0].data["data"]["simple"] == "simple" - assert self.calls[0].data["list"][0] == "list" + assert dict(self.calls[0].data) == { + "hello": "goodbye", + "effect": { + "value": "complex", + "simple": "simple", + }, + "list": ["list", "2"], + "entity_id": ["hello.world"], + "area_id": ["test-area-id"], + } def test_service_template_service_call(self): """Test legacy service_template call with templating.""" From 1162d9a752267ce493ff1252f417e31f4f54963a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Nov 2020 12:54:05 -1000 Subject: [PATCH 288/430] Create tables with a charset that can hold all expected data under mysql (#43732) By default these tables are created with utf8 which can only hold 3 bytes. This meant that all emjoi would trigger a MySQLdb._exceptions.OperationalError because they are 4 bytes. This will only fix the issue for users who recreate their tables. --- homeassistant/components/recorder/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 6c8b6050a9a..5b37f7e3f9d 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -42,6 +42,10 @@ ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHAN class Events(Base): # type: ignore """Event history data.""" + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } __tablename__ = TABLE_EVENTS event_id = Column(Integer, primary_key=True) event_type = Column(String(32)) @@ -96,6 +100,10 @@ class Events(Base): # type: ignore class States(Base): # type: ignore """State change history.""" + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } __tablename__ = TABLE_STATES state_id = Column(Integer, primary_key=True) domain = Column(String(64)) From e406a47293e9b0c6fafe372dd755d532cd96bcb6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Nov 2020 23:56:15 +0100 Subject: [PATCH 289/430] Allow configuring the delay in the motion light blueprint (#43737) --- .../automation/blueprints/motion_light.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index c923778156d..6cf368f9c0d 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -15,8 +15,16 @@ blueprint: target: entity: domain: light + no_motion_wait: + name: Wait time + description: Time to wait until the light should be turned off. + default: 120 + selector: + number: + min: 0 + max: 3600 -# If motion is detected within the 120s delay, +# If motion is detected within the delay, # we restart the script. mode: restart max_exceeded: silent @@ -35,6 +43,6 @@ action: entity_id: !placeholder motion_entity from: "on" to: "off" - - delay: 120 + - delay: !placeholder no_motion_wait - service: homeassistant.turn_off target: !placeholder light_target From 65ab8cbc717c6481707c2141f56a1a9bf34a5dd3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 29 Nov 2020 00:06:32 +0100 Subject: [PATCH 290/430] Add support for multiple tags and devices in tag trigger (#43098) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tag/trigger.py | 45 +++++++++++------- tests/components/tag/test_trigger.py | 63 ++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index 8da9baa5aaa..9803bd56afe 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -1,8 +1,8 @@ """Support for tag triggers.""" import voluptuous as vol -from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_PLATFORM +from homeassistant.core import HassJob from homeassistant.helpers import config_validation as cv from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID @@ -10,28 +10,39 @@ from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): DOMAIN, - vol.Required(TAG_ID): cv.string, - vol.Optional(DEVICE_ID): cv.string, + vol.Required(TAG_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ) async def async_attach_trigger(hass, config, action, automation_info): """Listen for tag_scanned events based on configuration.""" - tag_id = config.get(TAG_ID) - device_id = config.get(DEVICE_ID) - event_data = {TAG_ID: tag_id} + tag_ids = set(config[TAG_ID]) + device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None - if device_id: - event_data[DEVICE_ID] = device_id + job = HassJob(action) - event_config = { - event_trigger.CONF_PLATFORM: "event", - event_trigger.CONF_EVENT_TYPE: EVENT_TAG_SCANNED, - event_trigger.CONF_EVENT_DATA: event_data, - } - event_config = event_trigger.TRIGGER_SCHEMA(event_config) + async def handle_event(event): + """Listen for tag scan events and calls the action when data matches.""" + if event.data.get(TAG_ID) not in tag_ids or ( + device_ids is not None and event.data.get(DEVICE_ID) not in device_ids + ): + return - return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type=DOMAIN - ) + task = hass.async_run_hass_job( + job, + { + "trigger": { + "platform": DOMAIN, + "event": event, + "description": "Tag scanned", + } + }, + event.context, + ) + + if task: + await task + + return hass.bus.async_listen(EVENT_TAG_SCANNED, handle_event) diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 3a83c8e5d2b..9a97d95e7d5 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -4,7 +4,8 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.tag import async_scan_tag -from homeassistant.components.tag.const import DOMAIN, TAG_ID +from homeassistant.components.tag.const import DEVICE_ID, DOMAIN, TAG_ID +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -45,6 +46,7 @@ async def test_triggers(hass, tag_setup, calls): { automation.DOMAIN: [ { + "alias": "test", "trigger": {"platform": DOMAIN, TAG_ID: "abc123"}, "action": { "service": "test.automation", @@ -63,6 +65,18 @@ async def test_triggers(hass, tag_setup, calls): assert len(calls) == 1 assert calls[0].data["message"] == "service called" + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "automation.test"}, + blocking=True, + ) + + await async_scan_tag(hass, "abc123", None) + await hass.async_block_till_done() + + assert len(calls) == 1 + async def test_exception_bad_trigger(hass, calls, caplog): """Test for exception on event triggers firing.""" @@ -84,3 +98,50 @@ async def test_exception_bad_trigger(hass, calls, caplog): ) await hass.async_block_till_done() assert "Invalid config for [automation]" in caplog.text + + +async def test_multiple_tags_and_devices_trigger(hass, tag_setup, calls): + """Test multiple tags and devices triggers.""" + assert await tag_setup() + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": DOMAIN, + TAG_ID: ["abc123", "def456"], + DEVICE_ID: ["ghi789", "jkl0123"], + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + # Should not trigger + await async_scan_tag(hass, tag_id="abc123", device_id=None) + await async_scan_tag(hass, tag_id="abc123", device_id="invalid") + await hass.async_block_till_done() + + # Should trigger + await async_scan_tag(hass, tag_id="abc123", device_id="ghi789") + await hass.async_block_till_done() + await async_scan_tag(hass, tag_id="abc123", device_id="jkl0123") + await hass.async_block_till_done() + await async_scan_tag(hass, "def456", device_id="ghi789") + await hass.async_block_till_done() + await async_scan_tag(hass, "def456", device_id="jkl0123") + await hass.async_block_till_done() + + assert len(calls) == 4 + assert calls[0].data["message"] == "service called" + assert calls[1].data["message"] == "service called" + assert calls[2].data["message"] == "service called" + assert calls[3].data["message"] == "service called" From d5efb3e3de4f3d12c2f55976134c12f0f411b9b8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 29 Nov 2020 00:44:25 +0100 Subject: [PATCH 291/430] Remove temporary variable by only retrieving needed value (#42522) --- homeassistant/components/alexa/handlers.py | 4 +-- .../components/bmw_connected_drive/sensor.py | 2 +- .../components/broadlink/config_flow.py | 4 +-- homeassistant/components/cast/media_player.py | 32 +++++++++---------- homeassistant/components/doorbird/logbook.py | 2 +- homeassistant/components/mysensors/sensor.py | 4 +-- homeassistant/components/scsgate/__init__.py | 2 +- homeassistant/components/unifi/controller.py | 4 +-- homeassistant/components/xbox/media_source.py | 4 +-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 65c48ba0206..8837210b6ad 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -995,14 +995,14 @@ async def async_api_set_mode(hass, config, directive, context): # Fan Direction if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - _, direction = mode.split(".") + direction = mode.split(".")[1] if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - _, position = mode.split(".") + position = mode.split(".")[1] if position == cover.STATE_CLOSED: service = cover.SERVICE_CLOSE_COVER diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 995a6a6ef86..4668b1da6eb 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -125,7 +125,7 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - _, unit = self._attribute_info.get(self._attribute, [None, None]) + unit = self._attribute_info.get(self._attribute, [None, None])[1] return unit @property diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index e1a294d746d..a2e770d6c4f 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -41,8 +41,8 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define a device for the config flow.""" supported_types = { device_type - for _, device_types in DOMAINS_AND_TYPES - for device_type in device_types + for device_types in DOMAINS_AND_TYPES + for device_type in device_types[1] } if device.type not in supported_types: _LOGGER.error( diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 97956965b66..b76dbcaf20b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -601,7 +601,7 @@ class CastDevice(MediaPlayerEntity): @property def state(self): """Return the state of the player.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] if media_status is None: return None @@ -633,13 +633,13 @@ class CastDevice(MediaPlayerEntity): @property def media_content_id(self): """Content ID of current playing media.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.content_id if media_status else None @property def media_content_type(self): """Content type of current playing media.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] if media_status is None: return None if media_status.media_is_tvshow: @@ -653,13 +653,13 @@ class CastDevice(MediaPlayerEntity): @property def media_duration(self): """Duration of current playing media in seconds.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.duration if media_status else None @property def media_image_url(self): """Image url of current playing media.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] if media_status is None: return None @@ -677,49 +677,49 @@ class CastDevice(MediaPlayerEntity): @property def media_title(self): """Title of current playing media.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.title if media_status else None @property def media_artist(self): """Artist of current playing media (Music track only).""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.artist if media_status else None @property def media_album_name(self): """Album of current playing media (Music track only).""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.album_name if media_status else None @property def media_album_artist(self): """Album artist of current playing media (Music track only).""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.album_artist if media_status else None @property def media_track(self): """Track number of current playing media (Music track only).""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.track if media_status else None @property def media_series_title(self): """Return the title of the series of current playing media.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.series_title if media_status else None @property def media_season(self): """Season of current playing media (TV Show only).""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.season if media_status else None @property def media_episode(self): """Episode of current playing media (TV Show only).""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] return media_status.episode if media_status else None @property @@ -736,7 +736,7 @@ class CastDevice(MediaPlayerEntity): def supported_features(self): """Flag media player features that are supported.""" support = SUPPORT_CAST - media_status, _ = self._media_status() + media_status = self._media_status()[0] if media_status: if media_status.supports_queue_next: @@ -754,7 +754,7 @@ class CastDevice(MediaPlayerEntity): @property def media_position(self): """Position of current playing media in seconds.""" - media_status, _ = self._media_status() + media_status = self._media_status()[0] if media_status is None or not ( media_status.player_is_playing or media_status.player_is_paused @@ -769,7 +769,7 @@ class CastDevice(MediaPlayerEntity): Returns value from homeassistant.util.dt.utcnow(). """ - _, media_status_recevied = self._media_status() + media_status_recevied = self._media_status()[1] return media_status_recevied @property diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index c7ed802b7ea..a4889360d81 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -13,7 +13,7 @@ def async_describe_events(hass, async_describe_event): @callback def async_describe_logbook_event(event): """Describe a logbook event.""" - _, doorbird_event = event.event_type.split("_", 1) + doorbird_event = event.event_type.split("_", 1)[1] return { "name": "Doorbird", diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 6a6e95ddd01..bab6bf3fc40 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -85,7 +85,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity): @property def icon(self): """Return the icon to use in the frontend, if any.""" - _, icon = self._get_sensor_type() + icon = self._get_sensor_type()[1] return icon @property @@ -97,7 +97,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity): and set_req.V_UNIT_PREFIX in self._values ): return self._values[set_req.V_UNIT_PREFIX] - unit, _ = self._get_sensor_type() + unit = self._get_sensor_type()[0] return unit def _get_sensor_type(self): diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index f64106049de..6a190e64b7d 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -131,7 +131,7 @@ class SCSGate: with self._devices_to_register_lock: while self._devices_to_register: - _, device = self._devices_to_register.popitem() + device = self._devices_to_register.popitem()[1] self._devices[device.scs_id] = device self._device_being_registered = device.scs_id self._reactor.append_task(GetStatusTask(target=device.scs_id)) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 4d5bfa20215..a4435ccfecf 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -345,9 +345,9 @@ class UniFiController: mac = "" if entity.domain == TRACKER_DOMAIN: - mac, _ = entity.unique_id.split("-", 1) + mac = entity.unique_id.split("-", 1)[0] elif entity.domain == SWITCH_DOMAIN: - _, mac = entity.unique_id.split("-", 1) + mac = entity.unique_id.split("-", 1)[1] if mac in self.api.clients or mac not in self.api.clients_all: continue diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index 5615e11ea7f..750300e49ee 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -84,7 +84,7 @@ class XboxSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" _, category, url = async_parse_identifier(item) - _, kind = category.split("#", 1) + kind = category.split("#", 1)[1] return PlayMedia(url, MIME_TYPE_MAP[kind]) async def async_browse_media( @@ -267,7 +267,7 @@ def _build_categories(title): def _build_media_item(title: str, category: str, item: XboxMediaItem): """Build individual media item.""" - _, kind = category.split("#", 1) + kind = category.split("#", 1)[1] return BrowseMediaSource( domain=DOMAIN, identifier=f"{title}~~{category}~~{item.uri}", From 5462d6e79818947bb866bd5a53daba9e9a35fe4f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 29 Nov 2020 00:04:09 +0000 Subject: [PATCH 292/430] [ci skip] Translation update --- .../components/abode/translations/cs.json | 17 +++++++++++++++- .../components/abode/translations/es.json | 17 +++++++++++++++- .../components/abode/translations/et.json | 17 +++++++++++++++- .../components/abode/translations/pl.json | 17 +++++++++++++++- .../components/august/translations/et.json | 2 +- .../components/awair/translations/et.json | 2 +- .../azure_devops/translations/et.json | 2 +- .../fireservicerota/translations/et.json | 2 +- .../motion_blinds/translations/pl.json | 20 +++++++++++++++++++ .../simplisafe/translations/et.json | 2 +- .../components/solaredge/translations/pl.json | 7 ++++++- .../components/spotify/translations/es.json | 5 +++++ .../components/spotify/translations/et.json | 5 +++++ .../components/spotify/translations/pl.json | 5 +++++ .../spotify/translations/zh-Hant.json | 5 +++++ 15 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/motion_blinds/translations/pl.json diff --git a/homeassistant/components/abode/translations/cs.json b/homeassistant/components/abode/translations/cs.json index 36ff5f8b08f..30ffaa74a32 100644 --- a/homeassistant/components/abode/translations/cs.json +++ b/homeassistant/components/abode/translations/cs.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_mfa_code": "Neplatn\u00fd k\u00f3d MFA" }, "step": { + "mfa": { + "data": { + "mfa_code": "K\u00f3d MFA (6 \u010d\u00edslic)" + }, + "title": "Zadejte k\u00f3d MFA pro Abode" + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "E-mail" + }, + "title": "Vypl\u0148te sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje do Abode" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index 2e5d21707b5..9fa8cd8b06b 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "La reautenticaci\u00f3n fue exitosa", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" }, "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Introduce tu c\u00f3digo MFA para Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electronico" + }, + "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/abode/translations/et.json b/homeassistant/components/abode/translations/et.json index 7c711e252c8..f44b4ae25c4 100644 --- a/homeassistant/components/abode/translations/et.json +++ b/homeassistant/components/abode/translations/et.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamise viga" + "invalid_auth": "Tuvastamise viga", + "invalid_mfa_code": "Kehtetu MFA-kood" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA kood (6-kohaline)" + }, + "title": "Sisesta oma Abode MFA kood" + }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "E-post" + }, + "title": "Sisesta oma Abode sisselogimisteave" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/abode/translations/pl.json b/homeassistant/components/abode/translations/pl.json index 8331a58090f..79966f14d9c 100644 --- a/homeassistant/components/abode/translations/pl.json +++ b/homeassistant/components/abode/translations/pl.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_mfa_code": "Nieprawid\u0142owy kod uwierzytelniania wielosk\u0142adnikowego" }, "step": { + "mfa": { + "data": { + "mfa_code": "6-cyfrowy kod uwierzytelniania wielosk\u0142adnikowego" + }, + "title": "Wprowad\u017a kod uwierzytelniania wielosk\u0142adnikowego dla Abode" + }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a informacje logowania Abode" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json index efff8b8fe0f..0b455b06d00 100644 --- a/homeassistant/components/august/translations/et.json +++ b/homeassistant/components/august/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Konto on juba seadistatud", - "reauth_successful": "Taasautentimine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index ad96a767f2a..374db23e18e 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Konto on juba seadistatud", "no_devices_found": "V\u00f5rgust ei leitud Awair seadmeid", - "reauth_successful": "Taasautentimine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", diff --git a/homeassistant/components/azure_devops/translations/et.json b/homeassistant/components/azure_devops/translations/et.json index f9f9334ec5c..63ec0276d89 100644 --- a/homeassistant/components/azure_devops/translations/et.json +++ b/homeassistant/components/azure_devops/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Konto on juba seadistatud", - "reauth_successful": "Taasautentimine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendus nurjus", diff --git a/homeassistant/components/fireservicerota/translations/et.json b/homeassistant/components/fireservicerota/translations/et.json index 27fc2a9b4d7..dedd74e8701 100644 --- a/homeassistant/components/fireservicerota/translations/et.json +++ b/homeassistant/components/fireservicerota/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kasutaja on juba seadistatud", - "reauth_successful": "Taasautentimine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json new file mode 100644 index 00000000000..8f73496fd1d --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "Motion Blinds", + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "host": "Adres IP" + }, + "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index f386228f404..b98a121046a 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", - "reauth_successful": "Taasautentimine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "identifier_exists": "Konto on juba registreeritud", diff --git a/homeassistant/components/solaredge/translations/pl.json b/homeassistant/components/solaredge/translations/pl.json index 0c217a12510..2fa4af72cb3 100644 --- a/homeassistant/components/solaredge/translations/pl.json +++ b/homeassistant/components/solaredge/translations/pl.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "site_exists": "Ten site_id jest ju\u017c skonfigurowany" }, "error": { - "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z API solaredge", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "site_exists": "Ten site_id jest ju\u017c skonfigurowany", + "site_not_active": "Strona nie jest aktywna" }, "step": { "user": { diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 68783fd3caf..ce1966a8edd 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -18,5 +18,10 @@ "title": "Volver a autenticar con Spotify" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API de Spotify" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json index f3ceec4fa5c..01583d1b0f0 100644 --- a/homeassistant/components/spotify/translations/et.json +++ b/homeassistant/components/spotify/translations/et.json @@ -18,5 +18,10 @@ "title": "Autendi Spotify uuesti" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API l\u00f5pp-punkt on k\u00e4ttesaadav" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index c503dfccda5..f9e6f429214 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -18,5 +18,10 @@ "title": "Ponownie uwierzytelnij integracj\u0119" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Dost\u0119pno\u015b\u0107 punktu ko\u0144cowego API Spotify" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index 9518c53732d..ce35507e661 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -18,5 +18,10 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API \u53ef\u9054\u7aef\u9ede" + } } } \ No newline at end of file From 493eaef616bfbb1d492d37fa6c4222f5862b7d00 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 29 Nov 2020 16:30:17 +0100 Subject: [PATCH 293/430] Bump pypck to v0.7.6 (#43710) * Bump pypck to v0.7.6 * Await commands to ensure that they are received. --- homeassistant/components/lcn/__init__.py | 1 - homeassistant/components/lcn/climate.py | 23 ++++++++---- homeassistant/components/lcn/cover.py | 42 ++++++++++++++-------- homeassistant/components/lcn/light.py | 31 +++++++++------- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lcn/scene.py | 4 ++- homeassistant/components/lcn/switch.py | 19 ++++++---- requirements_all.txt | 2 +- 8 files changed, 79 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 9cf91695d56..faba23a52b9 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -233,7 +233,6 @@ async def async_setup(hass, config): } connection = pypck.connection.PchkConnectionManager( - hass.loop, conf_connection[CONF_HOST], conf_connection[CONF_PORT], conf_connection[CONF_USERNAME], diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 9634dcf8fb3..f55734a052f 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -17,6 +17,8 @@ from .const import ( ) from .helpers import get_connection +PARALLEL_UPDATES = 0 + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None @@ -118,14 +120,20 @@ class LcnClimate(LcnDevice, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == const.HVAC_MODE_HEAT: + if not await self.address_connection.lock_regulator( + self.regulator_id, False + ): + return self._is_on = True - self.address_connection.lock_regulator(self.regulator_id, False) + self.async_write_ha_state() elif hvac_mode == const.HVAC_MODE_OFF: + if not await self.address_connection.lock_regulator( + self.regulator_id, True + ): + return self._is_on = False - self.address_connection.lock_regulator(self.regulator_id, True) self._target_temperature = None - - self.async_write_ha_state() + self.async_write_ha_state() async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -133,10 +141,11 @@ class LcnClimate(LcnDevice, ClimateEntity): if temperature is None: return - self._target_temperature = temperature - self.address_connection.var_abs( + if not await self.address_connection.var_abs( self.setpoint, self._target_temperature, self.unit - ) + ): + return + self._target_temperature = temperature self.async_write_ha_state() def input_received(self, input_obj): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 512a7978c8c..ae88441f89e 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -8,6 +8,8 @@ from . import LcnDevice from .const import CONF_CONNECTIONS, CONF_MOTOR, CONF_REVERSE_TIME, DATA_LCN from .helpers import get_connection +PARALLEL_UPDATES = 0 + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None @@ -86,27 +88,34 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_close_cover(self, **kwargs): """Close the cover.""" + state = pypck.lcn_defs.MotorStateModifier.DOWN + if not await self.address_connection.control_motors_outputs( + state, self.reverse_time + ): + return self._is_opening = False self._is_closing = True - state = pypck.lcn_defs.MotorStateModifier.DOWN - self.address_connection.control_motors_outputs(state, self.reverse_time) self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Open the cover.""" + state = pypck.lcn_defs.MotorStateModifier.UP + if not await self.address_connection.control_motors_outputs( + state, self.reverse_time + ): + return self._is_closed = False self._is_opening = True self._is_closing = False - state = pypck.lcn_defs.MotorStateModifier.UP - self.address_connection.control_motors_outputs(state, self.reverse_time) self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" + state = pypck.lcn_defs.MotorStateModifier.STOP + if not await self.address_connection.control_motors_outputs(state): + return self._is_closing = False self._is_opening = False - state = pypck.lcn_defs.MotorStateModifier.STOP - self.address_connection.control_motors_outputs(state) self.async_write_ha_state() def input_received(self, input_obj): @@ -176,30 +185,33 @@ class LcnRelayCover(LcnDevice, CoverEntity): async def async_close_cover(self, **kwargs): """Close the cover.""" - self._is_opening = False - self._is_closing = True states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN - self.address_connection.control_motors_relays(states) + if not await self.address_connection.control_motors_relays(states): + return + self._is_opening = False + self._is_closing = True self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Open the cover.""" + states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP + if not await self.address_connection.control_motors_relays(states): + return self._is_closed = False self._is_opening = True self._is_closing = False - states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP - self.address_connection.control_motors_relays(states) self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self._is_closing = False - self._is_opening = False states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP - self.address_connection.control_motors_relays(states) + if not await self.address_connection.control_motors_relays(states): + return + self._is_closing = False + self._is_opening = False self.async_write_ha_state() def input_received(self, input_obj): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 8fd24c43069..def025e0cf2 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -21,6 +21,8 @@ from .const import ( ) from .helpers import get_connection +PARALLEL_UPDATES = 0 + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None @@ -87,8 +89,6 @@ class LcnOutputLight(LcnDevice, LightEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - self._is_on = True - self._is_dimming_to_zero = False if ATTR_BRIGHTNESS in kwargs: percent = int(kwargs[ATTR_BRIGHTNESS] / 255.0 * 100) else: @@ -100,12 +100,16 @@ class LcnOutputLight(LcnDevice, LightEntity): else: transition = self._transition - self.address_connection.dim_output(self.output.value, percent, transition) + if not await self.address_connection.dim_output( + self.output.value, percent, transition + ): + return + self._is_on = True + self._is_dimming_to_zero = False self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - self._is_on = False if ATTR_TRANSITION in kwargs: transition = pypck.lcn_defs.time_to_ramp_value( kwargs[ATTR_TRANSITION] * 1000 @@ -113,9 +117,12 @@ class LcnOutputLight(LcnDevice, LightEntity): else: transition = self._transition + if not await self.address_connection.dim_output( + self.output.value, 0, transition + ): + return self._is_dimming_to_zero = bool(transition) - - self.address_connection.dim_output(self.output.value, 0, transition) + self._is_on = False self.async_write_ha_state() def input_received(self, input_obj): @@ -157,22 +164,22 @@ class LcnRelayLight(LcnDevice, LightEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - self._is_on = True states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON - self.address_connection.control_relays(states) - + if not await self.address_connection.control_relays(states): + return + self._is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - self._is_on = False states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF - self.address_connection.control_relays(states) - + if not await self.address_connection.control_relays(states): + return + self._is_on = False self.async_write_ha_state() def input_received(self, input_obj): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index dca4436d5c2..6811e086ecc 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,6 +2,6 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.4"], + "requirements": ["pypck==0.7.6"], "codeowners": ["@alengwenus"] } diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index 0bad1b87efe..cac13ee1653 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -18,6 +18,8 @@ from .const import ( ) from .helpers import get_connection +PARALLEL_UPDATES = 0 + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None @@ -67,7 +69,7 @@ class LcnScene(LcnDevice, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate scene.""" - self.address_connection.activate_scene( + await self.address_connection.activate_scene( self.register_id, self.scene_id, self.output_ports, diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index e441ce40383..1d6f7cb6df4 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -8,6 +8,8 @@ from . import LcnDevice from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS from .helpers import get_connection +PARALLEL_UPDATES = 0 + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None @@ -57,14 +59,16 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" + if not await self.address_connection.dim_output(self.output.value, 100, 0): + return self._is_on = True - self.address_connection.dim_output(self.output.value, 100, 0) self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" + if not await self.address_connection.dim_output(self.output.value, 0, 0): + return self._is_on = False - self.address_connection.dim_output(self.output.value, 0, 0) self.async_write_ha_state() def input_received(self, input_obj): @@ -102,20 +106,21 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - self._is_on = True - states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON - self.address_connection.control_relays(states) + if not await self.address_connection.control_relays(states): + return + self._is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - self._is_on = False states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF - self.address_connection.control_relays(states) + if not await self.address_connection.control_relays(states): + return + self._is_on = False self.async_write_ha_state() def input_received(self, input_obj): diff --git a/requirements_all.txt b/requirements_all.txt index 4cbbc82748b..02a5fe91dc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.4 +pypck==0.7.6 # homeassistant.components.pjlink pypjlink2==1.2.1 From 54425ae0f3c1e35c8040583edfe16ad3a1b7e243 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 29 Nov 2020 11:59:23 -0500 Subject: [PATCH 294/430] Use the correct property for full init event (#43745) --- homeassistant/components/zha/core/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index bec429cdf22..812ac168d48 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -569,7 +569,7 @@ class ZHAGateway: ) await self._async_device_joined(zha_device) - device_info = zha_device.device_info + device_info = zha_device.zha_device_info device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.INITIALIZED.name async_dispatcher_send( self._hass, From 6eeb9c0e49a1453c8c9c22975be76826485248bc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 29 Nov 2020 20:47:46 +0100 Subject: [PATCH 295/430] ZHA: remove unused 'from_cache' argument from 'async_get_state' and add 'async_update' (#42413) * remove unused 'from_cache' argument from 'async_get_state' * define async_update to use homeassistant.update_entity --- homeassistant/components/zha/light.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 1f310c32aab..6b3a39d0926 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -280,7 +280,7 @@ class BaseLight(LogMixin, light.LightEntity): 0x0, 0x0, 0x0, - 0x0, # update action only, action off, no dir,time,hue + 0x0, # update action only, action off, no dir, time, hue ) t_log["color_loop_set"] = result self._effect = None @@ -421,20 +421,20 @@ class Light(BaseLight, ZhaEntity): if "effect" in last_state.attributes: self._effect = last_state.attributes["effect"] - async def async_get_state(self, from_cache=True): - """Attempt to retrieve on off state from the light.""" - if not from_cache and not self.available: + async def async_get_state(self): + """Attempt to retrieve the state from the light.""" + if not self.available: return - self.debug("polling current state - from cache: %s", from_cache) + self.debug("polling current state") if self._on_off_channel: state = await self._on_off_channel.get_attribute_value( - "on_off", from_cache=from_cache + "on_off", from_cache=False ) if state is not None: self._state = state if self._level_channel: level = await self._level_channel.get_attribute_value( - "current_level", from_cache=from_cache + "current_level", from_cache=False ) if level is not None: self._brightness = level @@ -447,7 +447,7 @@ class Light(BaseLight, ZhaEntity): ] results = await self._color_channel.get_attributes( - attributes, from_cache=from_cache + attributes, from_cache=False ) color_temp = results.get("color_temperature") @@ -468,15 +468,19 @@ class Light(BaseLight, ZhaEntity): else: self._effect = None + async def async_update(self): + """Update to the latest state.""" + await self.async_get_state() + async def _refresh(self, time): """Call async_get_state at an interval.""" - await self.async_get_state(from_cache=False) + await self.async_get_state() self.async_write_ha_state() async def _maybe_force_refresh(self, signal): """Force update the state if the signal contains the entity id for this entity.""" if self.entity_id in signal["entity_ids"]: - await self.async_get_state(from_cache=False) + await self.async_get_state() self.async_write_ha_state() From eab6a0508b5c7fe0e41e8502102ac4eb119cd421 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sun, 29 Nov 2020 23:54:27 +0100 Subject: [PATCH 296/430] Address FireServiceRota late code review (#43741) * Address review comment from PR #38206 * Address review comment from PR #43638 * Address review comment from PR #43700 * isort fixed * Better code for duty entity update * Removed all pylint relative-beyond-top-level * Removed logger entry from entity state method --- .../components/fireservicerota/__init__.py | 24 +++++++++++-------- .../fireservicerota/binary_sensor.py | 2 -- .../components/fireservicerota/config_flow.py | 15 ++++++------ .../components/fireservicerota/sensor.py | 7 +++--- .../components/fireservicerota/switch.py | 11 +++------ .../fireservicerota/test_config_flow.py | 9 +++---- 6 files changed, 30 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 35bd71cf794..bf5f3f6beea 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -75,7 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FireServiceRota config entry.""" - hass.data[DOMAIN][entry.entry_id].websocket.stop_listener() + await hass.async_add_executor_job( + hass.data[DOMAIN][entry.entry_id].websocket.stop_listener + ) unload_ok = all( await asyncio.gather( @@ -115,7 +117,7 @@ class FireServiceRotaOauth: except (InvalidAuthError, InvalidTokenError): _LOGGER.error("Error refreshing tokens, triggered reauth workflow") - self._hass.add_job( + self._hass.async_create_task( self._hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, @@ -150,7 +152,7 @@ class FireServiceRotaWebSocket: self._entry = entry self._fsr_incidents = FireServiceRotaIncidents(on_incident=self._on_incident) - self._incident_data = None + self.incident_data = None def _construct_url(self) -> str: """Return URL with latest access token.""" @@ -158,14 +160,10 @@ class FireServiceRotaWebSocket: self._entry.data[CONF_URL], self._entry.data[CONF_TOKEN]["access_token"] ) - def incident_data(self) -> object: - """Return incident data.""" - return self._incident_data - def _on_incident(self, data) -> None: """Received new incident, update data.""" _LOGGER.debug("Received new incident via websocket: %s", data) - self._incident_data = data + self.incident_data = data dispatcher_send(self._hass, f"{DOMAIN}_{self._entry.entry_id}_update") def start_listener(self) -> None: @@ -190,6 +188,9 @@ class FireServiceRotaClient: self._url = entry.data[CONF_URL] self._tokens = entry.data[CONF_TOKEN] + self.entry_id = entry.entry_id + self.unique_id = entry.unique_id + self.token_refresh_failure = False self.incident_id = None self.on_duty = False @@ -216,12 +217,12 @@ class FireServiceRotaClient: try: return await self._hass.async_add_executor_job(func, *args) except (ExpiredTokenError, InvalidTokenError): - self.websocket.stop_listener() + await self._hass.async_add_executor_job(self.websocket.stop_listener) self.token_refresh_failure = True if await self.oauth.async_refresh_tokens(): self.token_refresh_failure = False - self.websocket.start_listener() + await self._hass.async_add_executor_job(self.websocket.start_listener) return await self._hass.async_add_executor_job(func, *args) @@ -231,6 +232,9 @@ class FireServiceRotaClient: self.fsr.get_availability, str(self._hass.config.time_zone) ) + if not data: + return + self.on_duty = bool(data.get("available")) _LOGGER.debug("Updated availability data: %s", data) diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index afbabcd98a3..fc06e605cbd 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -63,7 +63,6 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): self._state = self._client.on_duty - _LOGGER.debug("Set state of entity 'Duty Binary Sensor' to '%s'", self._state) return self._state @property @@ -89,5 +88,4 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): if key in data } - _LOGGER.debug("Set attributes of entity 'Duty Binary Sensor' to '%s'", attr) return attr diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index f815566c316..be986744d6c 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME -# pylint: disable=relative-beyond-top-level from .const import DOMAIN, URL_LIST DATA_SCHEMA = vol.Schema( @@ -57,14 +56,14 @@ class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(self._username) self._abort_if_unique_id_configured() - try: - self.api = FireServiceRota( - base_url=self._base_url, - username=self._username, - password=self._password, - ) - token_info = await self.hass.async_add_executor_job(self.api.request_tokens) + self.api = FireServiceRota( + base_url=self._base_url, + username=self._username, + password=self._password, + ) + try: + token_info = await self.hass.async_add_executor_job(self.api.request_tokens) except InvalidAuthError: self.api = None return self.async_show_form( diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index cb0ae761975..83272eff926 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -7,7 +7,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType -# pylint: disable=relative-beyond-top-level from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,8 +27,8 @@ class IncidentsSensor(RestoreEntity): def __init__(self, client): """Initialize.""" self._client = client - self._entry_id = self._client._entry.entry_id - self._unique_id = f"{self._client._entry.unique_id}_Incidents" + self._entry_id = self._client.entry_id + self._unique_id = f"{self._client.unique_id}_Incidents" self._state = None self._state_attributes = {} @@ -123,7 +122,7 @@ class IncidentsSensor(RestoreEntity): @callback def client_update(self) -> None: """Handle updated data from the data client.""" - data = self._client.websocket.incident_data() + data = self._client.websocket.incident_data if not data or "body" not in data: return diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index f7a6a13db2b..7519270ca5c 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -7,7 +7,6 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -# pylint: disable=relative-beyond-top-level from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -97,7 +96,6 @@ class ResponseSwitch(SwitchEntity): if key in data } - _LOGGER.debug("Set attributes of entity 'Response Switch' to '%s'", attr) return attr async def async_turn_on(self, **kwargs) -> None: @@ -128,12 +126,9 @@ class ResponseSwitch(SwitchEntity): self.client_update, ) ) - self.async_on_remove(self._coordinator.async_add_listener(self.on_duty_update)) - - @callback - def on_duty_update(self): - """Trigger on a on duty update.""" - self.async_write_ha_state() + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) @callback def client_update(self) -> None: diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index b826e6b303b..8ccaae5fbbc 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -2,9 +2,7 @@ from pyfireservicerota import InvalidAuthError from homeassistant import data_entry_flow -from homeassistant.components.fireservicerota.const import ( # pylint: disable=unused-import - DOMAIN, -) +from homeassistant.components.fireservicerota.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from tests.async_mock import patch @@ -16,7 +14,6 @@ MOCK_CONF = { CONF_URL: "www.brandweerrooster.nl", } - MOCK_DATA = { "auth_implementation": DOMAIN, CONF_URL: MOCK_CONF[CONF_URL], @@ -79,14 +76,14 @@ async def test_step_user(hass): with patch( "homeassistant.components.fireservicerota.config_flow.FireServiceRota" - ) as MockFireServiceRota, patch( + ) as mock_fsr, patch( "homeassistant.components.fireservicerota.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.fireservicerota.async_setup_entry", return_value=True, ) as mock_setup_entry: - mock_fireservicerota = MockFireServiceRota.return_value + mock_fireservicerota = mock_fsr.return_value mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO result = await hass.config_entries.flow.async_init( From 533f22bb76b9f9fec606fb35358d4691a546f1a3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 30 Nov 2020 00:03:45 +0000 Subject: [PATCH 297/430] [ci skip] Translation update --- .../components/abode/translations/it.json | 17 ++++++++++++++++- .../components/spotify/translations/it.json | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json index e1af6d27fc8..a3e5aa4d7a8 100644 --- a/homeassistant/components/abode/translations/it.json +++ b/homeassistant/components/abode/translations/it.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "La riautenticazione ha avuto successo", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida" + "invalid_auth": "Autenticazione non valida", + "invalid_mfa_code": "Codice MFA non valido" }, "step": { + "mfa": { + "data": { + "mfa_code": "Codice MFA (6 cifre)" + }, + "title": "Inserisci il tuo codice MFA per Abode" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "title": "Inserisci le tue informazioni di accesso Abode" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index c201c70cd1c..6911d38be00 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -18,5 +18,10 @@ "title": "Reautenticare l'integrazione" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint API Spotify raggiungibile" + } } } \ No newline at end of file From 2cbb93be431f6edfc1251b32f3de883d3145193b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Nov 2020 22:13:50 -1000 Subject: [PATCH 298/430] Always keep the current recorder run when purging (#43733) --- homeassistant/components/recorder/purge.py | 1 + tests/components/recorder/test_purge.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index fee4480e134..43e84785f7d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -68,6 +68,7 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: deleted_rows = ( session.query(RecorderRuns) .filter(RecorderRuns.start < purge_before) + .filter(RecorderRuns.run_id != instance.run_info.run_id) .delete(synchronize_session=False) ) _LOGGER.debug("Deleted %s recorder_runs", deleted_rows) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 91a2299e4b6..9cb07819e79 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -62,6 +62,22 @@ def test_purge_old_events(hass, hass_recorder): assert events.count() == 2 +def test_purge_old_recorder_runs(hass, hass_recorder): + """Test deleting old recorder runs keeps current run.""" + hass = hass_recorder() + _add_test_recorder_runs(hass) + + # make sure we start with 7 recorder runs + with session_scope(hass=hass) as session: + recorder_runs = session.query(RecorderRuns) + assert recorder_runs.count() == 7 + + # run purge_old_data() + finished = purge_old_data(hass.data[DATA_INSTANCE], 0, repack=False) + assert finished + assert recorder_runs.count() == 1 + + def test_purge_method(hass, hass_recorder): """Test purge method.""" hass = hass_recorder() From 945a0a9f7e654e66408aac30c6f7dfa6939c7d62 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 30 Nov 2020 00:19:42 -0800 Subject: [PATCH 299/430] Add nest device triggers for camera and doorbell events (#43548) --- .coveragerc | 13 +- homeassistant/components/nest/__init__.py | 29 +- homeassistant/components/nest/camera_sdm.py | 4 +- homeassistant/components/nest/climate_sdm.py | 4 +- homeassistant/components/nest/const.py | 1 + .../components/nest/device_trigger.py | 101 ++++++ homeassistant/components/nest/events.py | 49 +++ homeassistant/components/nest/sensor_sdm.py | 4 +- homeassistant/components/nest/strings.json | 16 +- .../components/nest/translations/en.json | 10 +- tests/components/nest/test_device_trigger.py | 313 ++++++++++++++++++ tests/components/nest/test_events.py | 12 +- 12 files changed, 520 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/nest/device_trigger.py create mode 100644 homeassistant/components/nest/events.py create mode 100644 tests/components/nest/test_device_trigger.py diff --git a/.coveragerc b/.coveragerc index 24d6d3fe651..f74bf42a7e3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -572,7 +572,18 @@ omit = homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py - homeassistant/components/nest/* + homeassistant/components/nest/__init__.py + homeassistant/components/nest/api.py + homeassistant/components/nest/binary_sensor.py + homeassistant/components/nest/camera.py + homeassistant/components/nest/camera_legacy.py + homeassistant/components/nest/camera_sdm.py + homeassistant/components/nest/climate.py + homeassistant/components/nest/climate_legacy.py + homeassistant/components/nest/climate_sdm.py + homeassistant/components/nest/local_auth.py + homeassistant/components/nest/sensor.py + homeassistant/components/nest/sensor_legacy.py homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py homeassistant/components/netatmo/camera.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 7c2564f01bb..97c9da5794b 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -5,14 +5,7 @@ from datetime import datetime, timedelta import logging import threading -from google_nest_sdm.event import ( - AsyncEventCallback, - CameraMotionEvent, - CameraPersonEvent, - CameraSoundEvent, - DoorbellChimeEvent, - EventMessage, -) +from google_nest_sdm.event import AsyncEventCallback, EventMessage from google_nest_sdm.exceptions import GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from nest import Nest @@ -50,24 +43,19 @@ from . import api, config_flow, local_auth from .const import ( API_URL, DATA_SDM, + DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, SIGNAL_NEST_UPDATE, ) +from .events import EVENT_NAME_MAP, NEST_EVENT _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" -NEST_EVENT = "nest_event" -EVENT_TRAIT_MAP = { - DoorbellChimeEvent.NAME: "DoorbellChime", - CameraMotionEvent.NAME: "CameraMotion", - CameraPersonEvent.NAME: "CameraPerson", - CameraSoundEvent.NAME: "CameraSound", -} # Configuration for the legacy nest API @@ -206,11 +194,12 @@ class SignalUpdateCallback(AsyncEventCallback): _LOGGER.debug("Ignoring event for unregistered device '%s'", device_id) return for event in events: - if event not in EVENT_TRAIT_MAP: + event_type = EVENT_NAME_MAP.get(event) + if not event_type: continue message = { "device_id": device_entry.id, - "type": EVENT_TRAIT_MAP[event], + "type": event_type, } self._hass.bus.async_fire(NEST_EVENT, message) @@ -254,7 +243,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): subscriber.stop_async() raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id] = subscriber + hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber for component in PLATFORMS: hass.async_create_task( @@ -270,7 +259,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): # Legacy API return True - subscriber = hass.data[DOMAIN][entry.entry_id] + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] subscriber.stop_async() unload_ok = all( await asyncio.gather( @@ -281,7 +270,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) + hass.data[DOMAIN].pop(DATA_SUBSCRIBER) return unload_ok diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index bd06fb0bd8d..cec35eeca29 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -18,7 +18,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow -from .const import DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE from .device_info import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -32,7 +32,7 @@ async def async_setup_sdm_entry( ) -> None: """Set up the cameras.""" - subscriber = hass.data[DOMAIN][entry.entry_id] + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] try: device_manager = await subscriber.async_get_device_manager() except GoogleNestException as err: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index f341b76c404..e56d35c1dff 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -39,7 +39,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE from .device_info import DeviceInfo # Mapping for sdm.devices.traits.ThermostatMode mode field @@ -81,7 +81,7 @@ async def async_setup_sdm_entry( ) -> None: """Set up the client entities.""" - subscriber = hass.data[DOMAIN][entry.entry_id] + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] try: device_manager = await subscriber.async_get_device_manager() except GoogleNestException as err: diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index b418df97bba..3aba9ef5a7e 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -2,6 +2,7 @@ DOMAIN = "nest" DATA_SDM = "sdm" +DATA_SUBSCRIBER = "subscriber" SIGNAL_NEST_UPDATE = "nest_update" diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py new file mode 100644 index 00000000000..199dcf425de --- /dev/null +++ b/homeassistant/components/nest/device_trigger.py @@ -0,0 +1,101 @@ +"""Provides device automations for Nest.""" +import logging +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_SUBSCRIBER, DOMAIN +from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT + +_LOGGER = logging.getLogger(__name__) + +DEVICE = "device" + +TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values()) + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str: + """Get the nest API device_id from the HomeAssistant device_id.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(device_id) + for (domain, unique_id) in device.identifiers: + if domain == DOMAIN: + return unique_id + return None + + +async def async_get_device_trigger_types( + hass: HomeAssistant, nest_device_id: str +) -> List[str]: + """List event triggers supported for a Nest device.""" + # All devices should have already been loaded so any failures here are + # "shouldn't happen" cases + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + device_manager = await subscriber.async_get_device_manager() + nest_device = device_manager.devices.get(nest_device_id) + if not nest_device: + raise InvalidDeviceAutomationConfig(f"Nest device not found {nest_device_id}") + + # Determine the set of event types based on the supported device traits + trigger_types = [] + for trait in nest_device.traits.keys(): + trigger_type = DEVICE_TRAIT_TRIGGER_MAP.get(trait) + if trigger_type: + trigger_types.append(trigger_type) + return trigger_types + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for a Nest device.""" + nest_device_id = await async_get_nest_device_id(hass, device_id) + if not nest_device_id: + raise InvalidDeviceAutomationConfig(f"Device not found {device_id}") + trigger_types = await async_get_device_trigger_types(hass, nest_device_id) + return [ + { + CONF_PLATFORM: DEVICE, + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger_type, + } + for trigger_type in trigger_types + ] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: NEST_EVENT, + event_trigger.CONF_EVENT_DATA: { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + CONF_TYPE: config[CONF_TYPE], + }, + } + ) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/nest/events.py b/homeassistant/components/nest/events.py new file mode 100644 index 00000000000..abfe688c71c --- /dev/null +++ b/homeassistant/components/nest/events.py @@ -0,0 +1,49 @@ +"""Library from Pub/sub messages, events and device triggers.""" + +from google_nest_sdm.camera_traits import ( + CameraMotionTrait, + CameraPersonTrait, + CameraSoundTrait, +) +from google_nest_sdm.doorbell_traits import DoorbellChimeTrait +from google_nest_sdm.event import ( + CameraMotionEvent, + CameraPersonEvent, + CameraSoundEvent, + DoorbellChimeEvent, +) + +NEST_EVENT = "nest_event" +# The nest_event namespace will fire events that are triggered from messages +# received via the Pub/Sub subscriber. +# +# An example event data payload: +# { +# "device_id": "enterprises/some/device/identifier" +# "event_type": "camera_motion" +# } +# +# The following event types are fired: +EVENT_DOORBELL_CHIME = "doorbell_chime" +EVENT_CAMERA_MOTION = "camera_motion" +EVENT_CAMERA_PERSON = "camera_person" +EVENT_CAMERA_SOUND = "camera_sound" + +# Mapping of supported device traits to home assistant event types. Devices +# that support these traits will generate Pub/Sub event messages in +# the EVENT_NAME_MAP +DEVICE_TRAIT_TRIGGER_MAP = { + DoorbellChimeTrait.NAME: EVENT_DOORBELL_CHIME, + CameraMotionTrait.NAME: EVENT_CAMERA_MOTION, + CameraPersonTrait.NAME: EVENT_CAMERA_PERSON, + CameraSoundTrait.NAME: EVENT_CAMERA_SOUND, +} + +# Mapping of incoming SDM Pub/Sub event message types to the home assistant +# event type to fire. +EVENT_NAME_MAP = { + DoorbellChimeEvent.NAME: EVENT_DOORBELL_CHIME, + CameraMotionEvent.NAME: EVENT_CAMERA_MOTION, + CameraPersonEvent.NAME: EVENT_CAMERA_PERSON, + CameraSoundEvent.NAME: EVENT_CAMERA_SOUND, +} diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index b2b9500a156..9009414c5b4 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE from .device_info import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ async def async_setup_sdm_entry( ) -> None: """Set up the sensors.""" - subscriber = hass.data[DOMAIN][entry.entry_id] + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] try: device_manager = await subscriber.async_get_device_manager() except GoogleNestException as err: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0ce9c902121..f945469e26f 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -7,12 +7,16 @@ "init": { "title": "Authentication Provider", "description": "[%key:common::config_flow::title::oauth2_pick_implementation%]", - "data": { "flow_impl": "Provider" } + "data": { + "flow_impl": "Provider" + } }, "link": { "title": "Link Nest Account", "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.", - "data": { "code": "[%key:common::config_flow::data::pin%]" } + "data": { + "code": "[%key:common::config_flow::data::pin%]" + } } }, "error": { @@ -31,5 +35,13 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "device_automation": { + "trigger_type": { + "camera_person": "Person detected", + "camera_motion": "Motion detected", + "camera_sound": "Sound detected", + "doorbell_chime": "Doorbell pressed" + } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 3df0a9fa76d..b30e878368a 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -36,5 +36,13 @@ "title": "Pick Authentication Method" } } + }, + "device_automation": { + "trigger_type": { + "camera_person": "Person detected", + "camera_motion": "Motion detected", + "camera_sound": "Sound detected", + "doorbell_chime": "Doorbell pressed" + } } -} \ No newline at end of file +} diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py new file mode 100644 index 00000000000..89dccf6c31e --- /dev/null +++ b/tests/components/nest/test_device_trigger.py @@ -0,0 +1,313 @@ +"""The tests for Nest device triggers.""" +from google_nest_sdm.device import Device +from google_nest_sdm.event import EventMessage +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.nest import DOMAIN, NEST_EVENT +from homeassistant.setup import async_setup_component + +from .common import async_setup_sdm_platform + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) + +DEVICE_ID = "some-device-id" +DEVICE_NAME = "My Camera" +DATA_MESSAGE = {"message": "service-called"} + + +def make_camera(device_id, name=DEVICE_NAME, traits={}): + """Create a nest camera.""" + traits = traits.copy() + traits.update( + { + "sdm.devices.traits.Info": { + "customName": name, + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + }, + } + ) + return Device.MakeDevice( + { + "name": device_id, + "type": "sdm.devices.types.CAMERA", + "traits": traits, + }, + auth=None, + ) + + +async def async_setup_camera(hass, devices=None): + """Set up the platform and prerequisites for testing available triggers.""" + if not devices: + devices = {DEVICE_ID: make_camera(device_id=DEVICE_ID)} + return await async_setup_sdm_platform(hass, "camera", devices) + + +async def setup_automation(hass, device_id, trigger_type): + """Set up an automation trigger for testing triggering.""" + return await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_id, + "type": trigger_type, + }, + "action": { + "service": "test.automation", + "data": DATA_MESSAGE, + }, + }, + ] + }, + ) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass): + """Test we get the expected triggers from a nest.""" + camera = make_camera( + device_id=DEVICE_ID, + traits={ + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraPerson": {}, + }, + ) + await async_setup_camera(hass, {DEVICE_ID: camera}) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_device( + {("nest", DEVICE_ID)}, connections={} + ) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "camera_motion", + "device_id": device_entry.id, + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "camera_person", + "device_id": device_entry.id, + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_multiple_devices(hass): + """Test we get the expected triggers from a nest.""" + camera1 = make_camera( + device_id="device-id-1", + name="Camera 1", + traits={ + "sdm.devices.traits.CameraSound": {}, + }, + ) + camera2 = make_camera( + device_id="device-id-2", + name="Camera 2", + traits={ + "sdm.devices.traits.DoorbellChime": {}, + }, + ) + await async_setup_camera(hass, {"device-id-1": camera1, "device-id-2": camera2}) + + registry = await hass.helpers.entity_registry.async_get_registry() + entry1 = registry.async_get("camera.camera_1") + assert entry1.unique_id == "device-id-1-camera" + entry2 = registry.async_get("camera.camera_2") + assert entry2.unique_id == "device-id-2-camera" + + triggers = await async_get_device_automations(hass, "trigger", entry1.device_id) + assert len(triggers) == 1 + assert { + "platform": "device", + "domain": DOMAIN, + "type": "camera_sound", + "device_id": entry1.device_id, + } == triggers[0] + + triggers = await async_get_device_automations(hass, "trigger", entry2.device_id) + assert len(triggers) == 1 + assert { + "platform": "device", + "domain": DOMAIN, + "type": "doorbell_chime", + "device_id": entry2.device_id, + } == triggers[0] + + +async def test_triggers_for_invalid_device_id(hass): + """Get triggers for a device not found in the API.""" + camera = make_camera( + device_id=DEVICE_ID, + traits={ + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraPerson": {}, + }, + ) + await async_setup_camera(hass, {DEVICE_ID: camera}) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_device( + {("nest", DEVICE_ID)}, connections={} + ) + assert device_entry is not None + + # Create an additional device that does not exist. Fetching supported + # triggers for an unknown device will fail. + assert len(device_entry.config_entries) == 1 + config_entry_id = next(iter(device_entry.config_entries)) + device_entry_2 = device_registry.async_get_or_create( + config_entry_id=config_entry_id, identifiers={(DOMAIN, "some-unknown-nest-id")} + ) + assert device_entry_2 is not None + + with pytest.raises(InvalidDeviceAutomationConfig): + await async_get_device_automations(hass, "trigger", device_entry_2.id) + + +async def test_no_triggers(hass): + """Test we get the expected triggers from a nest.""" + camera = make_camera(device_id=DEVICE_ID, traits={}) + await async_setup_camera(hass, {DEVICE_ID: camera}) + + registry = await hass.helpers.entity_registry.async_get_registry() + entry = registry.async_get("camera.my_camera") + assert entry.unique_id == "some-device-id-camera" + + triggers = await async_get_device_automations(hass, "trigger", entry.device_id) + assert [] == triggers + + +async def test_fires_on_camera_motion(hass, calls): + """Test camera_motion triggers firing.""" + assert await setup_automation(hass, DEVICE_ID, "camera_motion") + + message = {"device_id": DEVICE_ID, "type": "camera_motion"} + hass.bus.async_fire(NEST_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data == DATA_MESSAGE + + +async def test_fires_on_camera_person(hass, calls): + """Test camera_person triggers firing.""" + assert await setup_automation(hass, DEVICE_ID, "camera_person") + + message = {"device_id": DEVICE_ID, "type": "camera_person"} + hass.bus.async_fire(NEST_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data == DATA_MESSAGE + + +async def test_fires_on_camera_sound(hass, calls): + """Test camera_person triggers firing.""" + assert await setup_automation(hass, DEVICE_ID, "camera_sound") + + message = {"device_id": DEVICE_ID, "type": "camera_sound"} + hass.bus.async_fire(NEST_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data == DATA_MESSAGE + + +async def test_fires_on_doorbell_chime(hass, calls): + """Test doorbell_chime triggers firing.""" + assert await setup_automation(hass, DEVICE_ID, "doorbell_chime") + + message = {"device_id": DEVICE_ID, "type": "doorbell_chime"} + hass.bus.async_fire(NEST_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data == DATA_MESSAGE + + +async def test_trigger_for_wrong_device_id(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + assert await setup_automation(hass, DEVICE_ID, "camera_motion") + + message = {"device_id": "wrong-device-id", "type": "camera_motion"} + hass.bus.async_fire(NEST_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_trigger_for_wrong_event_type(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + assert await setup_automation(hass, DEVICE_ID, "camera_motion") + + message = {"device_id": DEVICE_ID, "type": "wrong-event-type"} + hass.bus.async_fire(NEST_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_subscriber_automation(hass, calls): + """Test end to end subscriber triggers automation.""" + camera = make_camera( + device_id=DEVICE_ID, + traits={ + "sdm.devices.traits.CameraMotion": {}, + }, + ) + subscriber = await async_setup_camera(hass, {DEVICE_ID: camera}) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_device( + {("nest", DEVICE_ID)}, connections={} + ) + + assert await setup_automation(hass, device_entry.id, "camera_motion") + + # Simulate a pubsub message received by the subscriber with a motion event + event = EventMessage( + { + "eventId": "some-event-id", + "timestamp": "2019-01-01T00:00:01Z", + "resourceUpdate": { + "name": DEVICE_ID, + "events": { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", + "eventId": "FWWVQVUdGNUlTU2V4MGV2aTNXV...", + }, + }, + }, + }, + auth=None, + ) + await subscriber.async_receive_event(event) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == DATA_MESSAGE diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index b4b670fefbf..12314f60561 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -110,7 +110,7 @@ async def test_doorbell_chime_event(hass): assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, - "type": "DoorbellChime", + "type": "doorbell_chime", } @@ -134,7 +134,7 @@ async def test_camera_motion_event(hass): assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, - "type": "CameraMotion", + "type": "camera_motion", } @@ -158,7 +158,7 @@ async def test_camera_sound_event(hass): assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, - "type": "CameraSound", + "type": "camera_sound", } @@ -182,7 +182,7 @@ async def test_camera_person_event(hass): assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, - "type": "CameraPerson", + "type": "camera_person", } @@ -215,11 +215,11 @@ async def test_camera_multiple_event(hass): assert len(events) == 2 assert events[0].data == { "device_id": entry.device_id, - "type": "CameraMotion", + "type": "camera_motion", } assert events[1].data == { "device_id": entry.device_id, - "type": "CameraPerson", + "type": "camera_person", } From f221bfae520c5dd1ebf414cc61d341c56e99150c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 30 Nov 2020 09:32:06 +0100 Subject: [PATCH 300/430] Report correct weather condition at night for OpenWeatherMap (#42982) --- .../components/openweathermap/const.py | 3 ++- .../weather_update_coordinator.py | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 36f51a99ee1..2eb23e23861 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -149,6 +149,7 @@ LANGUAGES = [ "zh_tw", "zu", ] +WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT = 800 CONDITION_CLASSES = { ATTR_CONDITION_CLOUDY: [803, 804], ATTR_CONDITION_FOG: [701, 741], @@ -160,7 +161,7 @@ CONDITION_CLASSES = { ATTR_CONDITION_RAINY: [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], ATTR_CONDITION_SNOWY: [600, 601, 602, 611, 612, 620, 621, 622], ATTR_CONDITION_SNOWY_RAINY: [511, 615, 616], - ATTR_CONDITION_SUNNY: [800], + ATTR_CONDITION_SUNNY: [WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT], ATTR_CONDITION_WINDY: [905, 951, 952, 953, 954, 955, 956, 957], ATTR_CONDITION_WINDY_VARIANT: [958, 959, 960, 961], ATTR_CONDITION_EXCEPTIONAL: [ diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 40dddc2e90d..b4ddb40c046 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -6,6 +6,8 @@ import async_timeout from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -14,7 +16,9 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, ) +from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt from .const import ( ATTR_API_CLOUDS, @@ -35,6 +39,7 @@ from .const import ( FORECAST_MODE_HOURLY, FORECAST_MODE_ONECALL_DAILY, FORECAST_MODE_ONECALL_HOURLY, + WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT, ) _LOGGER = logging.getLogger(__name__) @@ -139,7 +144,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ), ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"), ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), - ATTR_FORECAST_CONDITION: self._get_condition(entry.weather_code), + ATTR_FORECAST_CONDITION: self._get_condition( + entry.weather_code, entry.reference_time("unix") + ), } temperature_dict = entry.temperature("celsius") @@ -186,9 +193,17 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None return round(rain_value + snow_value, 1) - @staticmethod - def _get_condition(weather_code): + def _get_condition(self, weather_code, timestamp=None): """Get weather condition from weather data.""" + if weather_code == WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT: + + if timestamp: + timestamp = dt.utc_from_timestamp(timestamp) + + if sun.is_up(self.hass, timestamp): + return ATTR_CONDITION_SUNNY + return ATTR_CONDITION_CLEAR_NIGHT + return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0] From aade4e63b8c5d74ee24e3159094e333be5443480 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 30 Nov 2020 09:34:34 +0100 Subject: [PATCH 301/430] Support asking covers to stop using google assistant (#43537) --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/trait.py | 71 ++++++++++++++++--- tests/components/google_assistant/__init__.py | 15 +++- .../google_assistant/test_smart_home.py | 5 +- .../components/google_assistant/test_trait.py | 45 ++++++++++++ 5 files changed, 123 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index be69f020190..47ceabb20e8 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -104,6 +104,7 @@ ERR_UNSUPPORTED_INPUT = "unsupportedInput" ERR_ALREADY_DISARMED = "alreadyDisarmed" ERR_ALREADY_ARMED = "alreadyArmed" +ERR_ALREADY_STOPPED = "alreadyStopped" ERR_CHALLENGE_NEEDED = "challengeNeeded" ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index b8d21c0c77b..8790c3c7402 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -67,6 +67,7 @@ from .const import ( CHALLENGE_PIN_NEEDED, ERR_ALREADY_ARMED, ERR_ALREADY_DISARMED, + ERR_ALREADY_STOPPED, ERR_CHALLENGE_NOT_SETUP, ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, @@ -564,24 +565,49 @@ class StartStopTrait(_Trait): @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" - return domain == vacuum.DOMAIN + if domain == vacuum.DOMAIN: + return True + + if domain == cover.DOMAIN and features & cover.SUPPORT_STOP: + return True + + return False def sync_attributes(self): """Return StartStop attributes for a sync request.""" - return { - "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & vacuum.SUPPORT_PAUSE - != 0 - } + domain = self.state.domain + if domain == vacuum.DOMAIN: + return { + "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & vacuum.SUPPORT_PAUSE + != 0 + } + if domain == cover.DOMAIN: + return {} def query_attributes(self): """Return StartStop query attributes.""" - return { - "isRunning": self.state.state == vacuum.STATE_CLEANING, - "isPaused": self.state.state == vacuum.STATE_PAUSED, - } + domain = self.state.domain + state = self.state.state + + if domain == vacuum.DOMAIN: + return { + "isRunning": state == vacuum.STATE_CLEANING, + "isPaused": state == vacuum.STATE_PAUSED, + } + + if domain == cover.DOMAIN: + return {"isRunning": state in (cover.STATE_CLOSING, cover.STATE_OPENING)} async def execute(self, command, data, params, challenge): + """Execute a StartStop command.""" + domain = self.state.domain + if domain == vacuum.DOMAIN: + return await self._execute_vacuum(command, data, params, challenge) + if domain == cover.DOMAIN: + return await self._execute_cover(command, data, params, challenge) + + async def _execute_vacuum(self, command, data, params, challenge): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: if params["start"]: @@ -618,6 +644,31 @@ class StartStopTrait(_Trait): context=data.context, ) + async def _execute_cover(self, command, data, params, challenge): + """Execute a StartStop command.""" + if command == COMMAND_STARTSTOP: + if params["start"] is False: + if self.state.state in (cover.STATE_CLOSING, cover.STATE_OPENING): + await self.hass.services.async_call( + self.state.domain, + cover.SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) + else: + raise SmartHomeError( + ERR_ALREADY_STOPPED, "Cover is already stopped" + ) + else: + raise SmartHomeError( + ERR_NOT_SUPPORTED, "Starting a cover is not supported" + ) + else: + raise SmartHomeError( + ERR_NOT_SUPPORTED, f"Command {command} is not supported" + ) + @register_trait class TemperatureSettingTrait(_Trait): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f4e26a77f48..bbc5b92615c 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -136,14 +136,20 @@ DEMO_DEVICES = [ { "id": "cover.living_room_window", "name": {"name": "Living Room Window"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": "action.devices.types.BLINDS", "willReportState": False, }, { "id": "cover.hall_window", "name": {"name": "Hall Window"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": "action.devices.types.BLINDS", "willReportState": False, }, @@ -157,7 +163,10 @@ DEMO_DEVICES = [ { "id": "cover.kitchen_window", "name": {"name": "Kitchen Window"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": "action.devices.types.BLINDS", "willReportState": False, }, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3fcab2dc2a2..27e62fafc73 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -842,7 +842,10 @@ async def test_device_class_cover(hass, device_class, google_type): "attributes": {"discreteOnlyOpenClose": True}, "id": "cover.demo_sensor", "name": {"name": "Demo Sensor"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": google_type, "willReportState": False, } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 2ca2e6c8e6c..4946416e1c8 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -384,6 +384,51 @@ async def test_startstop_vacuum(hass): assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"} +async def test_startstop_covert(hass): + """Test startStop trait support for vacuum domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None + assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None) + + state = State( + "cover.bla", + cover.STATE_CLOSED, + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP}, + ) + + trt = trait.StartStopTrait( + hass, + state, + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == {} + + for state_value in (cover.STATE_CLOSING, cover.STATE_OPENING): + state.state = state_value + assert trt.query_attributes() == {"isRunning": True} + + stop_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_STOP_COVER) + await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {}) + assert len(stop_calls) == 1 + assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} + + for state_value in (cover.STATE_CLOSED, cover.STATE_OPEN): + state.state = state_value + assert trt.query_attributes() == {"isRunning": False} + + with pytest.raises(SmartHomeError, match="Cover is already stopped"): + await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {}) + + with pytest.raises(SmartHomeError, match="Starting a cover is not supported"): + await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {}) + + with pytest.raises( + SmartHomeError, + match="Command action.devices.commands.PauseUnpause is not supported", + ): + await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {"start": True}, {}) + + async def test_color_setting_color_light(hass): """Test ColorSpectrum trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None From e307e1315a958b330aac9e8681744ef66237b21e Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 30 Nov 2020 00:57:32 -0800 Subject: [PATCH 302/430] Fix updating of Tesla switches after command (#43754) closes #43454 --- homeassistant/components/tesla/switch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 9fcdbae0c1c..efcb955ebf8 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -30,11 +30,13 @@ class ChargerSwitch(TeslaDevice, SwitchEntity): """Send the on command.""" _LOGGER.debug("Enable charging: %s", self.name) await self.tesla_device.start_charge() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable charging for: %s", self.name) await self.tesla_device.stop_charge() + self.async_write_ha_state() @property def is_on(self): @@ -51,11 +53,13 @@ class RangeSwitch(TeslaDevice, SwitchEntity): """Send the on command.""" _LOGGER.debug("Enable max range charging: %s", self.name) await self.tesla_device.set_max() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable max range charging: %s", self.name) await self.tesla_device.set_standard() + self.async_write_ha_state() @property def is_on(self): @@ -87,11 +91,13 @@ class UpdateSwitch(TeslaDevice, SwitchEntity): """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) + self.async_write_ha_state() 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) + self.async_write_ha_state() @property def is_on(self): @@ -108,11 +114,13 @@ class SentryModeSwitch(TeslaDevice, SwitchEntity): """Send the on command.""" _LOGGER.debug("Enable sentry mode: %s", self.name) await self.tesla_device.enable_sentry_mode() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable sentry mode: %s", self.name) await self.tesla_device.disable_sentry_mode() + self.async_write_ha_state() @property def is_on(self): From 0de9e8e9527b096d3d2594b98421f54ae1a9e480 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Nov 2020 14:27:02 +0100 Subject: [PATCH 303/430] Allow specifying device_id as target (#43767) --- homeassistant/helpers/config_validation.py | 4 ++ homeassistant/helpers/service.py | 79 ++++++++++++++-------- tests/helpers/test_service.py | 15 +++- 3 files changed, 67 insertions(+), 31 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 37d7f312d94..dea8deec715 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -34,6 +34,7 @@ import voluptuous_serialize from homeassistant.const import ( ATTR_AREA_ID, + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_ABOVE, CONF_ALIAS, @@ -884,6 +885,9 @@ PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) ENTITY_SERVICE_FIELDS = { vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_DEVICE_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) + ), vol.Optional(ATTR_AREA_ID): vol.Any(ENTITY_MATCH_NONE, vol.All(ensure_list, [str])), } diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7aa4ac8b013..47918f31514 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -14,6 +14,7 @@ from typing import ( Set, Tuple, Union, + cast, ) import voluptuous as vol @@ -21,6 +22,7 @@ import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL from homeassistant.const import ( ATTR_AREA_ID, + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_TEMPLATE, @@ -35,9 +37,8 @@ from homeassistant.exceptions import ( Unauthorized, UnknownUser, ) -from homeassistant.helpers import template +from homeassistant.helpers import device_registry, entity_registry, template import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType from homeassistant.loader import ( MAX_LOAD_CONCURRENTLY, @@ -120,7 +121,7 @@ def async_prepare_call_from_config( else: domain_service = config[CONF_SERVICE_TEMPLATE] - if isinstance(domain_service, Template): + if isinstance(domain_service, template.Template): try: domain_service.hass = hass domain_service = domain_service.async_render(variables) @@ -217,17 +218,19 @@ async def async_extract_entity_ids( Will convert group entity ids to the entity ids it represents. """ entity_ids = service_call.data.get(ATTR_ENTITY_ID) + device_ids = service_call.data.get(ATTR_DEVICE_ID) area_ids = service_call.data.get(ATTR_AREA_ID) + selects_entity_ids = entity_ids not in (None, ENTITY_MATCH_NONE) + selects_device_ids = device_ids not in (None, ENTITY_MATCH_NONE) + selects_area_ids = area_ids not in (None, ENTITY_MATCH_NONE) + extracted: Set[str] = set() - if entity_ids in (None, ENTITY_MATCH_NONE) and area_ids in ( - None, - ENTITY_MATCH_NONE, - ): + if not selects_entity_ids and not selects_device_ids and not selects_area_ids: return extracted - if entity_ids and entity_ids != ENTITY_MATCH_NONE: + if selects_entity_ids: # Entity ID attr can be a list or a string if isinstance(entity_ids, str): entity_ids = [entity_ids] @@ -237,39 +240,55 @@ async def async_extract_entity_ids( extracted.update(entity_ids) - if area_ids and area_ids != ENTITY_MATCH_NONE: + if not selects_device_ids and not selects_area_ids: + return extracted + + dev_reg, ent_reg = cast( + Tuple[device_registry.DeviceRegistry, entity_registry.EntityRegistry], + await asyncio.gather( + device_registry.async_get_registry(hass), + entity_registry.async_get_registry(hass), + ), + ) + + if not selects_device_ids: + picked_devices = set() + elif isinstance(device_ids, str): + picked_devices = {device_ids} + else: + assert isinstance(device_ids, list) + picked_devices = set(device_ids) + + if selects_area_ids: if isinstance(area_ids, str): area_ids = [area_ids] - dev_reg, ent_reg = await asyncio.gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), - ) + assert isinstance(area_ids, list) + # Find entities tied to an area extracted.update( entry.entity_id for area_id in area_ids - for entry in hass.helpers.entity_registry.async_entries_for_area( - ent_reg, area_id - ) + for entry in entity_registry.async_entries_for_area(ent_reg, area_id) ) - devices = [ - device - for area_id in area_ids - for device in hass.helpers.device_registry.async_entries_for_area( - dev_reg, area_id - ) - ] - extracted.update( - entry.entity_id - for device in devices - for entry in hass.helpers.entity_registry.async_entries_for_device( - ent_reg, device.id, include_disabled_entities=True - ) - if not entry.area_id + picked_devices.update( + [ + device.id + for area_id in area_ids + for device in device_registry.async_entries_for_area(dev_reg, area_id) + ] ) + if not picked_devices: + return extracted + + extracted.update( + entity_entry.entity_id + for entity_entry in ent_reg.entities.values() + if not entity_entry.area_id and entity_entry.device_id in picked_devices + ) + return extracted diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 93125fba96d..f9b09b259ca 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -93,7 +93,7 @@ def area_mock(hass): hass.states.async_set("light.Kitchen", STATE_OFF) device_in_area = dev_reg.DeviceEntry(area_id="test-area") - device_no_area = dev_reg.DeviceEntry() + device_no_area = dev_reg.DeviceEntry(id="device-no-area-id") device_diff_area = dev_reg.DeviceEntry(area_id="diff-area") mock_device_registry( @@ -947,3 +947,16 @@ async def test_extract_from_service_area_id(hass, area_mock): "light.diff_area", "light.in_area", ] + + call = ha.ServiceCall( + "light", + "turn_on", + {"area_id": ["test-area", "diff-area"], "device_id": "device-no-area-id"}, + ) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 3 + assert sorted(ent.entity_id for ent in extracted) == [ + "light.diff_area", + "light.in_area", + "light.no_area", + ] From 3e24868a9e6aa2d552f7461c6a3df40d00893efb Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 30 Nov 2020 15:24:18 +0000 Subject: [PATCH 304/430] Add system health check to IPMA (#43762) --- homeassistant/components/ipma/strings.json | 5 ++++ .../components/ipma/system_health.py | 22 ++++++++++++++++++ tests/components/ipma/test_system_health.py | 23 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 homeassistant/components/ipma/system_health.py create mode 100644 tests/components/ipma/test_system_health.py diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index bf0cf7a9ff8..24f142938d0 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -13,5 +13,10 @@ } }, "error": { "name_exists": "Name already exists" } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API endpoint reachable" + } } } diff --git a/homeassistant/components/ipma/system_health.py b/homeassistant/components/ipma/system_health.py new file mode 100644 index 00000000000..cd783490c35 --- /dev/null +++ b/homeassistant/components/ipma/system_health.py @@ -0,0 +1,22 @@ +"""Provide info to system health.""" +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +IPMA_API_URL = "http://api.ipma.pt" + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + return { + "api_endpoint_reachable": system_health.async_check_can_reach_url( + hass, IPMA_API_URL + ) + } diff --git a/tests/components/ipma/test_system_health.py b/tests/components/ipma/test_system_health.py new file mode 100644 index 00000000000..301621514f9 --- /dev/null +++ b/tests/components/ipma/test_system_health.py @@ -0,0 +1,23 @@ +"""Test ipma system health.""" +import asyncio + +from homeassistant.components.ipma.system_health import IPMA_API_URL +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info + + +async def test_ipma_system_health(hass, aioclient_mock): + """Test ipma system health.""" + aioclient_mock.get(IPMA_API_URL, json={"result": "ok", "data": {}}) + + hass.config.components.add("ipma") + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, "ipma") + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"api_endpoint_reachable": "ok"} From 14d1466400ee37c41b5b57146666913a4feb5254 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 30 Nov 2020 16:40:43 +0100 Subject: [PATCH 305/430] Add device information to solarlog integration (#43680) * Update sensor.py * Changed as requested Thanks, tested and works ok. Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- homeassistant/components/solarlog/sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 8c0650f0f7e..6073d12815b 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import SCAN_INTERVAL, SENSOR_TYPES +from .const import DOMAIN, SCAN_INTERVAL, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -96,6 +96,15 @@ class SolarlogSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def device_info(self): + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self.entry_id)}, + "name": self.device_name, + "manufacturer": "Solar-Log", + } + def update(self): """Get the latest data from the sensor and update the state.""" self.data.update() From 7ad2a6be30274f7913e7031f8a6e1f2dfb8710f6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 30 Nov 2020 09:38:52 -0800 Subject: [PATCH 306/430] Add hyperion config options flow (#43673) --- homeassistant/components/hyperion/__init__.py | 173 +++++ .../components/hyperion/config_flow.py | 445 +++++++++++ homeassistant/components/hyperion/const.py | 24 + homeassistant/components/hyperion/light.py | 339 +++++++-- .../components/hyperion/manifest.json | 19 +- .../components/hyperion/strings.json | 52 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 6 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- setup.cfg | 2 +- tests/components/hyperion/__init__.py | 135 ++++ tests/components/hyperion/test_config_flow.py | 696 ++++++++++++++++++ tests/components/hyperion/test_light.py | 473 +++++++++--- 14 files changed, 2213 insertions(+), 156 deletions(-) create mode 100644 homeassistant/components/hyperion/config_flow.py create mode 100644 homeassistant/components/hyperion/const.py create mode 100644 homeassistant/components/hyperion/strings.json create mode 100644 tests/components/hyperion/test_config_flow.py diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 60a0a2d3210..13dd977b7dc 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -1 +1,174 @@ """The Hyperion component.""" + +import asyncio +import logging +from typing import Any, Optional + +from hyperion import client, const as hyperion_const +from pkg_resources import parse_version + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import ( + CONF_ON_UNLOAD, + CONF_ROOT_CLIENT, + DOMAIN, + HYPERION_RELEASES_URL, + HYPERION_VERSION_WARN_CUTOFF, + SIGNAL_INSTANCES_UPDATED, +) + +PLATFORMS = [LIGHT_DOMAIN] + +_LOGGER = logging.getLogger(__name__) + +# Unique ID +# ========= +# A config entry represents a connection to a single Hyperion server. The config entry +# unique_id is the server id returned from the Hyperion instance (a unique ID per +# server). +# +# Each server connection may create multiple entities. The unique_id for each entity is +# __, where will be the unique_id on the +# relevant config entry (as above), will be the server instance # and +# will be a unique identifying type name for each entity associated with this +# server/instance (e.g. "hyperion_light"). +# +# The get_hyperion_unique_id method will create a per-entity unique id when given the +# server id, an instance number and a name. + +# hass.data format +# ================ +# +# hass.data[DOMAIN] = { +# : { +# "ROOT_CLIENT": , +# "ON_UNLOAD": [, ...], +# } +# } + + +def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str: + """Get a unique_id for a Hyperion instance.""" + return f"{server_id}_{instance}_{name}" + + +def create_hyperion_client( + *args: Any, + **kwargs: Any, +) -> client.HyperionClient: + """Create a Hyperion Client.""" + return client.HyperionClient(*args, **kwargs) + + +async def async_create_connect_hyperion_client( + *args: Any, + **kwargs: Any, +) -> Optional[client.HyperionClient]: + """Create and connect a Hyperion Client.""" + hyperion_client = create_hyperion_client(*args, **kwargs) + + if not await hyperion_client.async_client_connect(): + return None + return hyperion_client + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Hyperion component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up Hyperion from a config entry.""" + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + token = config_entry.data.get(CONF_TOKEN) + + hyperion_client = await async_create_connect_hyperion_client( + host, port, token=token + ) + if not hyperion_client: + raise ConfigEntryNotReady + version = await hyperion_client.async_sysinfo_version() + if version is not None: + try: + if parse_version(version) < parse_version(HYPERION_VERSION_WARN_CUTOFF): + _LOGGER.warning( + "Using a Hyperion server version < %s is not recommended -- " + "some features may be unavailable or may not function correctly. " + "Please consider upgrading: %s", + HYPERION_VERSION_WARN_CUTOFF, + HYPERION_RELEASES_URL, + ) + except ValueError: + pass + + hyperion_client.set_callbacks( + { + f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: ( + async_dispatcher_send( + hass, + SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id), + json, + ) + ) + } + ) + + hass.data[DOMAIN][config_entry.entry_id] = { + CONF_ROOT_CLIENT: hyperion_client, + CONF_ON_UNLOAD: [], + } + + # Must only listen for option updates after the setup is complete, as otherwise + # the YAML->ConfigEntry migration code triggers an options update, which causes a + # reload -- which clashes with the initial load (causing entity_id / unique_id + # clashes). + async def setup_then_listen() -> None: + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(config_entry, component) + for component in PLATFORMS + ] + ) + hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( + config_entry.add_update_listener(_async_options_updated) + ) + + hass.async_create_task(setup_then_listen()) + return True + + +async def _async_options_updated( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: + config_data = hass.data[DOMAIN].pop(config_entry.entry_id) + for func in config_data[CONF_ON_UNLOAD]: + func() + root_client = config_data[CONF_ROOT_CLIENT] + await root_client.async_client_disconnect() + return unload_ok diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py new file mode 100644 index 00000000000..aef74e530b1 --- /dev/null +++ b/homeassistant/components/hyperion/config_flow.py @@ -0,0 +1,445 @@ +"""Hyperion config flow.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Dict, Optional +from urllib.parse import urlparse + +from hyperion import client, const +import voluptuous as vol + +from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_PUSH, + ConfigEntry, + ConfigFlow, + OptionsFlow, +) +from homeassistant.const import CONF_BASE, CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType + +from . import create_hyperion_client + +# pylint: disable=unused-import +from .const import ( + CONF_AUTH_ID, + CONF_CREATE_TOKEN, + CONF_PRIORITY, + DEFAULT_ORIGIN, + DEFAULT_PRIORITY, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +# +------------------+ +------------------+ +--------------------+ +# |Step: SSDP | |Step: user | |Step: import | +# | | | | | | +# |Input: | |Input: | |Input: | +# +------------------+ +------------------+ +--------------------+ +# v v v +# +----------------------+-----------------------+ +# Auth not | Auth | +# required? | required? | +# | v +# | +------------+ +# | |Step: auth | +# | | | +# | |Input: token| +# | +------------+ +# | Static | +# v token | +# <------------------+ +# | | +# | | New token +# | v +# | +------------------+ +# | |Step: create_token| +# | +------------------+ +# | | +# | v +# | +---------------------------+ +--------------------------------+ +# | |Step: create_token_external|-->|Step: create_token_external_fail| +# | +---------------------------+ +--------------------------------+ +# | | +# | v +# | +-----------------------------------+ +# | |Step: create_token_external_success| +# | +-----------------------------------+ +# | | +# v<------------------+ +# | +# v +# +-------------+ Confirm not required? +# |Step: Confirm|---------------------->+ +# +-------------+ | +# | | +# v SSDP: Explicit confirm | +# +------------------------------>+ +# | +# v +# +----------------+ +# | Create! | +# +----------------+ + +# A note on choice of discovery mechanisms: Hyperion supports both Zeroconf and SSDP out +# of the box. This config flow needs two port numbers from the Hyperion instance, the +# JSON port (for the API) and the UI port (for the user to approve dynamically created +# auth tokens). With Zeroconf the port numbers for both are in different Zeroconf +# entries, and as Home Assistant only passes a single entry into the config flow, we can +# only conveniently 'see' one port or the other (which means we need to guess one port +# number). With SSDP, we get the combined block including both port numbers, so SSDP is +# the favored discovery implementation. + + +class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a Hyperion config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH + + def __init__(self) -> None: + """Instantiate config flow.""" + self._data: Dict[str, Any] = {} + self._request_token_task: Optional[asyncio.Task] = None + self._auth_id: Optional[str] = None + self._require_confirm: bool = False + self._port_ui: int = const.DEFAULT_PORT_UI + + def _create_client(self, raw_connection: bool = False) -> client.HyperionClient: + """Create and connect a client instance.""" + return create_hyperion_client( + self._data[CONF_HOST], + self._data[CONF_PORT], + token=self._data.get(CONF_TOKEN), + raw_connection=raw_connection, + ) + + async def _advance_to_auth_step_if_necessary( + self, hyperion_client: client.HyperionClient + ) -> Dict[str, Any]: + """Determine if auth is required.""" + auth_resp = await hyperion_client.async_is_auth_required() + + # Could not determine if auth is required. + if not auth_resp or not client.ResponseOK(auth_resp): + return self.async_abort(reason="auth_required_error") + auth_required = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_REQUIRED, False) + if auth_required: + return await self.async_step_auth() + return await self.async_step_confirm() + + async def async_step_import(self, import_data: ConfigType) -> Dict[str, Any]: + """Handle a flow initiated by a YAML config import.""" + self._data.update(import_data) + async with self._create_client(raw_connection=True) as hyperion_client: + if not hyperion_client: + return self.async_abort(reason="cannot_connect") + return await self._advance_to_auth_step_if_necessary(hyperion_client) + + async def async_step_ssdp( # type: ignore[override] + self, discovery_info: Dict[str, Any] + ) -> Dict[str, Any]: + """Handle a flow initiated by SSDP.""" + # Sample data provided by SSDP: { + # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', + # 'ssdp_st': 'upnp:rootdevice', + # 'deviceType': 'urn:schemas-upnp-org:device:Basic:1', + # 'friendlyName': 'Hyperion (192.168.0.1)', + # 'manufacturer': 'Hyperion Open Source Ambient Lighting', + # 'manufacturerURL': 'https://www.hyperion-project.org', + # 'modelDescription': 'Hyperion Open Source Ambient Light', + # 'modelName': 'Hyperion', + # 'modelNumber': '2.0.0-alpha.8', + # 'modelURL': 'https://www.hyperion-project.org', + # 'serialNumber': 'f9aab089-f85a-55cf-b7c1-222a72faebe9', + # 'UDN': 'uuid:f9aab089-f85a-55cf-b7c1-222a72faebe9', + # 'ports': { + # 'jsonServer': '19444', + # 'sslServer': '8092', + # 'protoBuffer': '19445', + # 'flatBuffer': '19400' + # }, + # 'presentationURL': 'index.html', + # 'iconList': { + # 'icon': { + # 'mimetype': 'image/png', + # 'height': '100', + # 'width': '100', + # 'depth': '32', + # 'url': 'img/hyperion/ssdp_icon.png' + # } + # }, + # 'ssdp_usn': 'uuid:f9aab089-f85a-55cf-b7c1-222a72faebe9', + # 'ssdp_ext': '', + # 'ssdp_server': 'Raspbian GNU/Linux 10 (buster)/10 UPnP/1.0 Hyperion/2.0.0-alpha.8'} + + # SSDP requires user confirmation. + self._require_confirm = True + self._data[CONF_HOST] = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + try: + self._port_ui = urlparse(discovery_info[ATTR_SSDP_LOCATION]).port + except ValueError: + self._port_ui = const.DEFAULT_PORT_UI + + try: + self._data[CONF_PORT] = int( + discovery_info.get("ports", {}).get( + "jsonServer", const.DEFAULT_PORT_JSON + ) + ) + except ValueError: + self._data[CONF_PORT] = const.DEFAULT_PORT_JSON + + hyperion_id = discovery_info.get(ATTR_UPNP_SERIAL) + if not hyperion_id: + return self.async_abort(reason="no_id") + + # For discovery mechanisms, we set the unique_id as early as possible to + # avoid discovery popping up a duplicate on the screen. The unique_id is set + # authoritatively later in the flow by asking the server to confirm its id + # (which should theoretically be the same as specified here) + await self.async_set_unique_id(hyperion_id) + self._abort_if_unique_id_configured() + + async with self._create_client(raw_connection=True) as hyperion_client: + if not hyperion_client: + return self.async_abort(reason="cannot_connect") + return await self._advance_to_auth_step_if_necessary(hyperion_client) + + # pylint: disable=arguments-differ + async def async_step_user( + self, + user_input: Optional[ConfigType] = None, + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + errors = {} + if user_input: + self._data.update(user_input) + + async with self._create_client(raw_connection=True) as hyperion_client: + if hyperion_client: + return await self._advance_to_auth_step_if_necessary( + hyperion_client + ) + errors[CONF_BASE] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=const.DEFAULT_PORT_JSON): int, + } + ), + errors=errors, + ) + + async def _cancel_request_token_task(self) -> None: + """Cancel the request token task if it exists.""" + if self._request_token_task is not None: + if not self._request_token_task.done(): + self._request_token_task.cancel() + + try: + await self._request_token_task + except asyncio.CancelledError: + pass + self._request_token_task = None + + async def _request_token_task_func(self, auth_id: str) -> None: + """Send an async_request_token request.""" + auth_resp: Optional[Dict[str, Any]] = None + async with self._create_client(raw_connection=True) as hyperion_client: + if hyperion_client: + # The Hyperion-py client has a default timeout of 3 minutes on this request. + auth_resp = await hyperion_client.async_request_token( + comment=DEFAULT_ORIGIN, id=auth_id + ) + assert self.hass + await self.hass.config_entries.flow.async_configure( + flow_id=self.flow_id, user_input=auth_resp + ) + + def _get_hyperion_url(self) -> str: + """Return the URL of the Hyperion UI.""" + # If this flow was kicked off by SSDP, this will be the correct frontend URL. If + # this is a manual flow instantiation, then it will be a best guess (as this + # flow does not have that information available to it). This is only used for + # approving new dynamically created tokens, so the complexity of asking the user + # manually for this information is likely not worth it (when it would only be + # used to open a URL, that the user already knows the address of). + return f"http://{self._data[CONF_HOST]}:{self._port_ui}" + + async def _can_login(self) -> Optional[bool]: + """Verify login details.""" + async with self._create_client(raw_connection=True) as hyperion_client: + if not hyperion_client: + return None + return bool( + client.LoginResponseOK( + await hyperion_client.async_login(token=self._data[CONF_TOKEN]) + ) + ) + + async def async_step_auth( + self, + user_input: Optional[ConfigType] = None, + ) -> Dict[str, Any]: + """Handle the auth step of a flow.""" + errors = {} + if user_input: + if user_input.get(CONF_CREATE_TOKEN): + return await self.async_step_create_token() + + # Using a static token. + self._data[CONF_TOKEN] = user_input.get(CONF_TOKEN) + login_ok = await self._can_login() + if login_ok is None: + return self.async_abort(reason="cannot_connect") + if login_ok: + return await self.async_step_confirm() + errors[CONF_BASE] = "invalid_access_token" + + return self.async_show_form( + step_id="auth", + data_schema=vol.Schema( + { + vol.Required(CONF_CREATE_TOKEN): bool, + vol.Optional(CONF_TOKEN): str, + } + ), + errors=errors, + ) + + async def async_step_create_token( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Send a request for a new token.""" + if user_input is None: + self._auth_id = client.generate_random_auth_id() + return self.async_show_form( + step_id="create_token", + description_placeholders={ + CONF_AUTH_ID: self._auth_id, + }, + ) + + # Cancel the request token task if it's already running, then re-create it. + await self._cancel_request_token_task() + # Start a task in the background requesting a new token. The next step will + # wait on the response (which includes the user needing to visit the Hyperion + # UI to approve the request for a new token). + assert self.hass + assert self._auth_id is not None + self._request_token_task = self.hass.async_create_task( + self._request_token_task_func(self._auth_id) + ) + return self.async_external_step( + step_id="create_token_external", url=self._get_hyperion_url() + ) + + async def async_step_create_token_external( + self, auth_resp: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle completion of the request for a new token.""" + if auth_resp is not None and client.ResponseOK(auth_resp): + token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN) + if token: + self._data[CONF_TOKEN] = token + return self.async_external_step_done( + next_step_id="create_token_success" + ) + return self.async_external_step_done(next_step_id="create_token_fail") + + async def async_step_create_token_success( + self, _: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Create an entry after successful token creation.""" + # Clean-up the request task. + await self._cancel_request_token_task() + + # Test the token. + login_ok = await self._can_login() + + if login_ok is None: + return self.async_abort(reason="cannot_connect") + if not login_ok: + return self.async_abort(reason="auth_new_token_not_work_error") + return await self.async_step_confirm() + + async def async_step_create_token_fail( + self, _: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Show an error on the auth form.""" + # Clean-up the request task. + await self._cancel_request_token_task() + return self.async_abort(reason="auth_new_token_not_granted_error") + + async def async_step_confirm( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Get final confirmation before entry creation.""" + if user_input is None and self._require_confirm: + return self.async_show_form( + step_id="confirm", + description_placeholders={ + CONF_HOST: self._data[CONF_HOST], + CONF_PORT: self._data[CONF_PORT], + CONF_ID: self.unique_id, + }, + ) + + async with self._create_client() as hyperion_client: + if not hyperion_client: + return self.async_abort(reason="cannot_connect") + hyperion_id = await hyperion_client.async_sysinfo_id() + + if not hyperion_id: + return self.async_abort(reason="no_id") + + await self.async_set_unique_id(hyperion_id, raise_on_progress=False) + self._abort_if_unique_id_configured() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + return self.async_create_entry( + title=f"{self._data[CONF_HOST]}:{self._data[CONF_PORT]}", data=self._data + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> HyperionOptionsFlow: + """Get the Hyperion Options flow.""" + return HyperionOptionsFlow(config_entry) + + +class HyperionOptionsFlow(OptionsFlow): + """Hyperion options flow.""" + + def __init__(self, config_entry: ConfigEntry): + """Initialize a Hyperion options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Manage the options.""" + if user_input is not 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_PRIORITY, + default=self._config_entry.options.get( + CONF_PRIORITY, DEFAULT_PRIORITY + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), + } + ), + ) diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py new file mode 100644 index 00000000000..9875f3bd918 --- /dev/null +++ b/homeassistant/components/hyperion/const.py @@ -0,0 +1,24 @@ +"""Constants for Hyperion integration.""" +DOMAIN = "hyperion" + +DEFAULT_NAME = "Hyperion" +DEFAULT_ORIGIN = "Home Assistant" +DEFAULT_PRIORITY = 128 + +CONF_AUTH_ID = "auth_id" +CONF_CREATE_TOKEN = "create_token" +CONF_INSTANCE = "instance" +CONF_PRIORITY = "priority" + +CONF_ROOT_CLIENT = "ROOT_CLIENT" +CONF_ON_UNLOAD = "ON_UNLOAD" + +SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}" +SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}" + +SOURCE_IMPORT = "import" + +HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" +HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" + +TYPE_HYPERION_LIGHT = "hyperion_light" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index e68ad8e31ee..90e362b3b16 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,28 +1,59 @@ """Support for Hyperion-NG remotes.""" +from __future__ import annotations + import logging +import re +from types import MappingProxyType +from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, cast from hyperion import client, const import voluptuous as vol +from homeassistant import data_entry_flow from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, + DOMAIN as LIGHT_DOMAIN, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, LightEntity, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) import homeassistant.util.color as color_util +from . import async_create_connect_hyperion_client, get_hyperion_unique_id +from .const import ( + CONF_ON_UNLOAD, + CONF_PRIORITY, + CONF_ROOT_CLIENT, + DEFAULT_ORIGIN, + DEFAULT_PRIORITY, + DOMAIN, + SIGNAL_INSTANCE_REMOVED, + SIGNAL_INSTANCES_UPDATED, + SOURCE_IMPORT, + TYPE_HYPERION_LIGHT, +) + _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_COLOR = "default_color" -CONF_PRIORITY = "priority" CONF_HDMI_PRIORITY = "hdmi_priority" CONF_EFFECT_LIST = "effect_list" @@ -35,21 +66,26 @@ CONF_EFFECT_LIST = "effect_list" # showing a solid color. This is the same method used by WLED. KEY_EFFECT_SOLID = "Solid" +KEY_ENTRY_ID_YAML = "YAML" + DEFAULT_COLOR = [255, 255, 255] DEFAULT_BRIGHTNESS = 255 DEFAULT_EFFECT = KEY_EFFECT_SOLID DEFAULT_NAME = "Hyperion" -DEFAULT_ORIGIN = "Home Assistant" -DEFAULT_PORT = 19444 -DEFAULT_PRIORITY = 128 +DEFAULT_PORT = const.DEFAULT_PORT_JSON DEFAULT_HDMI_PRIORITY = 880 -DEFAULT_EFFECT_LIST = [] +DEFAULT_EFFECT_LIST: List[str] = [] SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT +# Usage of YAML for configuration of the Hyperion component is deprecated. PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"), + cv.deprecated(CONF_HOST), + cv.deprecated(CONF_PORT), cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"), + cv.deprecated(CONF_NAME), + cv.deprecated(CONF_PRIORITY), cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"), PLATFORM_SCHEMA.extend( { @@ -77,96 +113,277 @@ ICON_EFFECT = "mdi:lava-lamp" ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a Hyperion server remote.""" - name = config[CONF_NAME] +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable, + discovery_info: Optional[DiscoveryInfoType] = None, +) -> None: + """Set up Hyperion platform..""" + + # This is the entrypoint for the old YAML-style Hyperion integration. The goal here + # is to auto-convert the YAML configuration into a config entry, with no human + # interaction, preserving the entity_id. This should be possible, as the YAML + # configuration did not support any of the things that should otherwise require + # human interaction in the config flow (e.g. it did not support auth). + host = config[CONF_HOST] port = config[CONF_PORT] - priority = config[CONF_PRIORITY] + instance = 0 # YAML only supports a single instance. - hyperion_client = client.HyperionClient(host, port) - - if not await hyperion_client.async_client_connect(): + # First, connect to the server and get the server id (which will be unique_id on a config_entry + # if there is one). + hyperion_client = await async_create_connect_hyperion_client(host, port) + if not hyperion_client: + raise PlatformNotReady + hyperion_id = await hyperion_client.async_sysinfo_id() + if not hyperion_id: raise PlatformNotReady - async_add_entities([Hyperion(name, priority, hyperion_client)]) + future_unique_id = get_hyperion_unique_id( + hyperion_id, instance, TYPE_HYPERION_LIGHT + ) + + # Possibility 1: Already converted. + # There is already a config entry with the unique id reporting by the + # server. Nothing to do here. + for entry in hass.config_entries.async_entries(domain=DOMAIN): + if entry.unique_id == hyperion_id: + return + + # Possibility 2: Upgraded to the new Hyperion component pre-config-flow. + # No config entry for this unique_id, but have an entity_registry entry + # with an old-style unique_id: + # :- (instance will always be 0, as YAML + # configuration does not support multiple + # instances) + # The unique_id needs to be updated, then the config_flow should do the rest. + registry = await async_get_registry(hass) + for entity_id, entity in registry.entities.items(): + if entity.config_entry_id is not None or entity.platform != DOMAIN: + continue + result = re.search(rf"([^:]+):(\d+)-{instance}", entity.unique_id) + if result and result.group(1) == host and int(result.group(2)) == port: + registry.async_update_entity(entity_id, new_unique_id=future_unique_id) + break + else: + # Possibility 3: This is the first upgrade to the new Hyperion component. + # No config entry and no entity_registry entry, in which case the CONF_NAME + # variable will be used as the preferred name. Rather than pollute the config + # entry with a "suggested name" type variable, instead create an entry in the + # registry that will subsequently be used when the entity is created with this + # unique_id. + + # This also covers the case that should not occur in the wild (no config entry, + # but new style unique_id). + registry.async_get_or_create( + domain=LIGHT_DOMAIN, + platform=DOMAIN, + unique_id=future_unique_id, + suggested_object_id=config[CONF_NAME], + ) + + async def migrate_yaml_to_config_entry_and_options( + host: str, port: int, priority: int + ) -> None: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: host, + CONF_PORT: port, + }, + ) + if ( + result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY + or result.get("result") is None + ): + _LOGGER.warning( + "Could not automatically migrate Hyperion YAML to a config entry." + ) + return + config_entry = result.get("result") + options = {**config_entry.options, CONF_PRIORITY: config[CONF_PRIORITY]} + hass.config_entries.async_update_entry(config_entry, options=options) + + _LOGGER.info( + "Successfully migrated Hyperion YAML configuration to a config entry." + ) + + # Kick off a config flow to create the config entry. + hass.async_create_task( + migrate_yaml_to_config_entry_and_options(host, port, config[CONF_PRIORITY]) + ) -class Hyperion(LightEntity): +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +) -> bool: + """Set up a Hyperion platform from config entry.""" + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + token = config_entry.data.get(CONF_TOKEN) + + async def async_instances_to_entities(response: Dict[str, Any]) -> None: + if not response or const.KEY_DATA not in response: + return + await async_instances_to_entities_raw(response[const.KEY_DATA]) + + async def async_instances_to_entities_raw(instances: List[Dict[str, Any]]) -> None: + registry = await async_get_registry(hass) + entities_to_add: List[HyperionLight] = [] + desired_unique_ids: Set[str] = set() + server_id = cast(str, config_entry.unique_id) + + # In practice, an instance can be in 3 states as seen by this function: + # + # * Exists, and is running: Add it to hass. + # * Exists, but is not running: Cannot add yet, but should not delete it either. + # It will show up as "unavailable". + # * No longer exists: Delete it from hass. + + # Add instances that are missing. + for instance in instances: + instance_id = instance.get(const.KEY_INSTANCE) + if instance_id is None or not instance.get(const.KEY_RUNNING, False): + continue + unique_id = get_hyperion_unique_id( + server_id, instance_id, TYPE_HYPERION_LIGHT + ) + desired_unique_ids.add(unique_id) + if unique_id in current_entities: + continue + hyperion_client = await async_create_connect_hyperion_client( + host, port, instance=instance_id, token=token + ) + if not hyperion_client: + continue + current_entities.add(unique_id) + entities_to_add.append( + HyperionLight( + unique_id, + instance.get(const.KEY_FRIENDLY_NAME, DEFAULT_NAME), + config_entry.options, + hyperion_client, + ) + ) + + # Delete instances that are no longer present on this server. + for unique_id in current_entities - desired_unique_ids: + current_entities.remove(unique_id) + async_dispatcher_send(hass, SIGNAL_INSTANCE_REMOVED.format(unique_id)) + entity_id = registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, unique_id) + if entity_id: + registry.async_remove(entity_id) + + async_add_entities(entities_to_add) + + # Readability note: This variable is kept alive in the context of the callback to + # async_instances_to_entities below. + current_entities: Set[str] = set() + + await async_instances_to_entities_raw( + hass.data[DOMAIN][config_entry.entry_id][CONF_ROOT_CLIENT].instances, + ) + hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( + async_dispatcher_connect( + hass, + SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id), + async_instances_to_entities, + ) + ) + return True + + +class HyperionLight(LightEntity): """Representation of a Hyperion remote.""" - def __init__(self, name, priority, hyperion_client): + def __init__( + self, + unique_id: str, + name: str, + options: MappingProxyType[str, Any], + hyperion_client: client.HyperionClient, + ) -> None: """Initialize the light.""" + self._unique_id = unique_id self._name = name - self._priority = priority + self._options = options self._client = hyperion_client # Active state representing the Hyperion instance. - self._set_internal_state( - brightness=255, rgb_color=DEFAULT_COLOR, effect=KEY_EFFECT_SOLID - ) - self._effect_list = [] + self._brightness: int = 255 + self._rgb_color: Sequence[int] = DEFAULT_COLOR + self._effect: str = KEY_EFFECT_SOLID + self._icon: str = ICON_LIGHTBULB + + self._effect_list: List[str] = [] @property - def should_poll(self): + def should_poll(self) -> bool: """Return whether or not this entity should be polled.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the light.""" return self._name @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return self._brightness @property - def hs_color(self): + def hs_color(self) -> Tuple[float, float]: """Return last color value set.""" return color_util.color_RGB_to_hs(*self._rgb_color) @property - def is_on(self): + def is_on(self) -> bool: """Return true if not black.""" - return self._client.is_on() + return bool(self._client.is_on()) @property - def icon(self): + def icon(self) -> str: """Return state specific icon.""" return self._icon @property - def effect(self): + def effect(self) -> str: """Return the current effect.""" return self._effect @property - def effect_list(self): + def effect_list(self) -> List[str]: """Return the list of supported effects.""" return ( self._effect_list - + const.KEY_COMPONENTID_EXTERNAL_SOURCES + + list(const.KEY_COMPONENTID_EXTERNAL_SOURCES) + [KEY_EFFECT_SOLID] ) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_HYPERION @property - def available(self): + def available(self) -> bool: """Return server availability.""" - return self._client.has_loaded_state + return bool(self._client.has_loaded_state) @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique id for this instance.""" - return self._client.id + return self._unique_id - async def async_turn_on(self, **kwargs): + def _get_option(self, key: str) -> Any: + """Get a value from the provided options.""" + defaults = {CONF_PRIORITY: DEFAULT_PRIORITY} + return self._options.get(key, defaults[key]) + + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the lights on.""" # == Turn device on == # Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be @@ -197,6 +414,7 @@ class Hyperion(LightEntity): # == Get key parameters == brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) effect = kwargs.get(ATTR_EFFECT, self._effect) + rgb_color: Sequence[int] if ATTR_HS_COLOR in kwargs: rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) else: @@ -220,7 +438,7 @@ class Hyperion(LightEntity): # Clear any color/effect. if not await self._client.async_send_clear( - **{const.KEY_PRIORITY: self._priority} + **{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)} ): return @@ -241,13 +459,13 @@ class Hyperion(LightEntity): # This call should not be necessary, but without it there is no priorities-update issued: # https://github.com/hyperion-project/hyperion.ng/issues/992 if not await self._client.async_send_clear( - **{const.KEY_PRIORITY: self._priority} + **{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)} ): return if not await self._client.async_send_set_effect( **{ - const.KEY_PRIORITY: self._priority, + const.KEY_PRIORITY: self._get_option(CONF_PRIORITY), const.KEY_EFFECT: {const.KEY_NAME: effect}, const.KEY_ORIGIN: DEFAULT_ORIGIN, } @@ -257,14 +475,14 @@ class Hyperion(LightEntity): else: if not await self._client.async_send_set_color( **{ - const.KEY_PRIORITY: self._priority, + const.KEY_PRIORITY: self._get_option(CONF_PRIORITY), const.KEY_COLOR: rgb_color, const.KEY_ORIGIN: DEFAULT_ORIGIN, } ): return - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Disable the LED output component.""" if not await self._client.async_send_set_component( **{ @@ -276,7 +494,12 @@ class Hyperion(LightEntity): ): return - def _set_internal_state(self, brightness=None, rgb_color=None, effect=None): + def _set_internal_state( + self, + brightness: Optional[int] = None, + rgb_color: Optional[Sequence[int]] = None, + effect: Optional[str] = None, + ) -> None: """Set the internal state.""" if brightness is not None: self._brightness = brightness @@ -291,11 +514,11 @@ class Hyperion(LightEntity): else: self._icon = ICON_EFFECT - def _update_components(self, _=None): + def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion components.""" self.async_write_ha_state() - def _update_adjustment(self, _=None): + def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion adjustments.""" if self._client.adjustment: brightness_pct = self._client.adjustment[0].get( @@ -308,7 +531,7 @@ class Hyperion(LightEntity): ) self.async_write_ha_state() - def _update_priorities(self, _=None): + def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion priorities.""" visible_priority = self._client.visible_priority if visible_priority: @@ -328,11 +551,11 @@ class Hyperion(LightEntity): ) self.async_write_ha_state() - def _update_effect_list(self, _=None): + def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion effects.""" if not self._client.effects: return - effect_list = [] + effect_list: List[str] = [] for effect in self._client.effects or []: if const.KEY_NAME in effect: effect_list.append(effect[const.KEY_NAME]) @@ -340,7 +563,7 @@ class Hyperion(LightEntity): self._effect_list = effect_list self.async_write_ha_state() - def _update_full_state(self): + def _update_full_state(self) -> None: """Update full Hyperion state.""" self._update_adjustment() self._update_priorities() @@ -356,12 +579,21 @@ class Hyperion(LightEntity): self._rgb_color, ) - def _update_client(self, json): + def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None: """Update client connection state.""" self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks when entity added to hass.""" + assert self.hass + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_INSTANCE_REMOVED.format(self._unique_id), + self.async_remove, + ) + ) + self._client.set_callbacks( { f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment, @@ -374,4 +606,7 @@ class Hyperion(LightEntity): # Load initial state. self._update_full_state() - return True + + async def async_will_remove_from_hass(self) -> None: + """Disconnect from server.""" + await self._client.async_client_disconnect() diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 4a9bf2ada8c..d8c6a2c352e 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -1,7 +1,18 @@ { + "codeowners": [ + "@dermotduffy" + ], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/hyperion", "domain": "hyperion", "name": "Hyperion", - "documentation": "https://www.home-assistant.io/integrations/hyperion", - "requirements": ["hyperion-py==0.3.0"], - "codeowners": ["@dermotduffy"] -} + "requirements": [ + "hyperion-py==0.6.0" + ], + "ssdp": [ + { + "manufacturer": "Hyperion Open Source Ambient Lighting", + "st": "urn:hyperion-project.org:device:basic:1" + } + ] +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json new file mode 100644 index 00000000000..180f266f1af --- /dev/null +++ b/homeassistant/components/hyperion/strings.json @@ -0,0 +1,52 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + }, + "auth": { + "description": "Configure authorization to your Hyperion Ambilight server", + "data": { + "create_token": "Automatically create new token", + "token": "Or provide pre-existing token" + } + }, + "create_token": { + "description": "Choose **Submit** below to request a new authentication token. You will be redirected to the Hyperion UI to approve the request. Please verify the shown id is \"{auth_id}\"", + "title": "Automatically create new authentication token" + }, + "create_token_external": { + "title": "Accept new token in Hyperion UI" + }, + "confirm": { + "description": "Do you want to add this Hyperion Ambilight to Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Confirm addition of Hyperion Ambilight service" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" + }, + "abort": { + "auth_required_error": "Failed to determine if authorization is required", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI", + "auth_new_token_not_work_error": "Failed to authenticate using newly created token", + "no_id": "The Hyperion Ambilight instance did not report its id" + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion priority to use for colors and effects" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a0d9cc2dd79..d9559e9085f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -92,6 +92,7 @@ FLOWS = [ "hue", "hunterdouglas_powerview", "hvv_departures", + "hyperion", "iaqualink", "icloud", "ifttt", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index f66c5f0999d..1617cd35435 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -114,6 +114,12 @@ SSDP = { "modelName": "Philips hue bridge 2015" } ], + "hyperion": [ + { + "manufacturer": "Hyperion Open Source Ambient Lighting", + "st": "urn:hyperion-project.org:device:basic:1" + } + ], "isy994": [ { "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1", diff --git a/requirements_all.txt b/requirements_all.txt index 02a5fe91dc5..ce8a2dcc93d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ huawei-lte-api==1.4.12 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.3.0 +hyperion-py==0.6.0 # homeassistant.components.bh1750 # homeassistant.components.bme280 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a28cd56d3de..721eb3acfb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.12 # homeassistant.components.hyperion -hyperion-py==0.3.0 +hyperion-py==0.6.0 # homeassistant.components.iaqualink iaqualink==0.3.4 diff --git a/setup.cfg b/setup.cfg index 8286e58c7cf..6ff4e1abb12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,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.huawei_lte.*,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.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,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.huawei_lte.*,homeassistant.components.hyperion.*,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.*,tests.components.hyperion.*] strict = true ignore_errors = false warn_unreachable = true diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index e4c1ee67efa..a2febcca2a5 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -1 +1,136 @@ """Tests for the Hyperion component.""" +from __future__ import annotations + +import logging +from types import TracebackType +from typing import Any, Dict, Optional, Type + +from hyperion import const + +from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN +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.helpers.typing import HomeAssistantType + +from tests.async_mock import AsyncMock, Mock, patch # type: ignore[attr-defined] +from tests.common import MockConfigEntry + +TEST_HOST = "test" +TEST_PORT = const.DEFAULT_PORT_JSON + 1 +TEST_PORT_UI = const.DEFAULT_PORT_UI + 1 +TEST_INSTANCE = 1 +TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9" +TEST_SYSINFO_VERSION = "2.0.0-alpha.8" +TEST_PRIORITY = 180 +TEST_YAML_NAME = f"{TEST_HOST}_{TEST_PORT}_{TEST_INSTANCE}" +TEST_YAML_ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_YAML_NAME}" +TEST_ENTITY_ID_1 = "light.test_instance_1" +TEST_ENTITY_ID_2 = "light.test_instance_2" +TEST_ENTITY_ID_3 = "light.test_instance_3" +TEST_TITLE = f"{TEST_HOST}:{TEST_PORT}" + +TEST_TOKEN = "sekr1t" +TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c" +TEST_CONFIG_ENTRY_OPTIONS: Dict[str, Any] = {CONF_PRIORITY: TEST_PRIORITY} + +TEST_INSTANCE_1: Dict[str, Any] = { + "friendly_name": "Test instance 1", + "instance": 1, + "running": True, +} +TEST_INSTANCE_2: Dict[str, Any] = { + "friendly_name": "Test instance 2", + "instance": 2, + "running": True, +} +TEST_INSTANCE_3: Dict[str, Any] = { + "friendly_name": "Test instance 3", + "instance": 3, + "running": True, +} + +_LOGGER = logging.getLogger(__name__) + + +class AsyncContextManagerMock(Mock): # type: ignore[misc] + """An async context manager mock for Hyperion.""" + + async def __aenter__(self) -> Optional[AsyncContextManagerMock]: + """Enter context manager and connect the client.""" + result = await self.async_client_connect() + return self if result else None + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + """Leave context manager and disconnect the client.""" + await self.async_client_disconnect() + + +def create_mock_client() -> Mock: + """Create a mock Hyperion client.""" + mock_client = AsyncContextManagerMock() + # pylint: disable=attribute-defined-outside-init + mock_client.async_client_connect = AsyncMock(return_value=True) + mock_client.async_client_disconnect = AsyncMock(return_value=True) + mock_client.async_is_auth_required = AsyncMock( + return_value={ + "command": "authorize-tokenRequired", + "info": {"required": False}, + "success": True, + "tan": 1, + } + ) + mock_client.async_login = AsyncMock( + return_value={"command": "authorize-login", "success": True, "tan": 0} + ) + + mock_client.async_sysinfo_id = AsyncMock(return_value=TEST_SYSINFO_ID) + mock_client.async_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_ID) + mock_client.adjustment = None + mock_client.effects = None + mock_client.instances = [ + {"friendly_name": "Test instance 1", "instance": 0, "running": True} + ] + + return mock_client + + +def add_test_config_entry(hass: HomeAssistantType) -> ConfigEntry: + """Add a test config entry.""" + config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + entry_id=TEST_CONFIG_ENTRY_ID, + domain=DOMAIN, + data={ + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + }, + title=f"Hyperion {TEST_SYSINFO_ID}", + unique_id=TEST_SYSINFO_ID, + options=TEST_CONFIG_ENTRY_OPTIONS, + ) + config_entry.add_to_hass(hass) # type: ignore[no-untyped-call] + return config_entry + + +async def setup_test_config_entry( + hass: HomeAssistantType, hyperion_client: Optional[Mock] = None +) -> ConfigEntry: + """Add a test Hyperion entity to hass.""" + config_entry = add_test_config_entry(hass) + + hyperion_client = hyperion_client or create_mock_client() + # pylint: disable=attribute-defined-outside-init + hyperion_client.instances = [TEST_INSTANCE_1] + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=hyperion_client, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py new file mode 100644 index 00000000000..807a3829e7b --- /dev/null +++ b/tests/components/hyperion/test_config_flow.py @@ -0,0 +1,696 @@ +"""Tests for the Hyperion config flow.""" + +import logging +from typing import Any, Dict, Optional + +from hyperion import const + +from homeassistant import data_entry_flow +from homeassistant.components.hyperion.const import ( + CONF_AUTH_ID, + CONF_CREATE_TOKEN, + CONF_PRIORITY, + DOMAIN, + SOURCE_IMPORT, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_PORT, + CONF_TOKEN, + SERVICE_TURN_ON, +) +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + TEST_CONFIG_ENTRY_ID, + TEST_ENTITY_ID_1, + TEST_HOST, + TEST_INSTANCE, + TEST_PORT, + TEST_PORT_UI, + TEST_SYSINFO_ID, + TEST_TITLE, + TEST_TOKEN, + add_test_config_entry, + create_mock_client, +) + +from tests.async_mock import AsyncMock, patch # type: ignore[attr-defined] +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + +TEST_IP_ADDRESS = "192.168.0.1" +TEST_HOST_PORT: Dict[str, Any] = { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, +} + +TEST_AUTH_REQUIRED_RESP = { + "command": "authorize-tokenRequired", + "info": { + "required": True, + }, + "success": True, + "tan": 1, +} + +TEST_AUTH_ID = "ABCDE" +TEST_REQUEST_TOKEN_SUCCESS = { + "command": "authorize-requestToken", + "success": True, + "info": {"comment": const.DEFAULT_ORIGIN, "id": TEST_AUTH_ID, "token": TEST_TOKEN}, +} + +TEST_REQUEST_TOKEN_FAIL = { + "command": "authorize-requestToken", + "success": False, + "error": "Token request timeout or denied", +} + +TEST_SSDP_SERVICE_INFO = { + "ssdp_location": f"http://{TEST_HOST}:{TEST_PORT_UI}/description.xml", + "ssdp_st": "upnp:rootdevice", + "deviceType": "urn:schemas-upnp-org:device:Basic:1", + "friendlyName": f"Hyperion ({TEST_HOST})", + "manufacturer": "Hyperion Open Source Ambient Lighting", + "manufacturerURL": "https://www.hyperion-project.org", + "modelDescription": "Hyperion Open Source Ambient Light", + "modelName": "Hyperion", + "modelNumber": "2.0.0-alpha.8", + "modelURL": "https://www.hyperion-project.org", + "serialNumber": f"{TEST_SYSINFO_ID}", + "UDN": f"uuid:{TEST_SYSINFO_ID}", + "ports": { + "jsonServer": f"{TEST_PORT}", + "sslServer": "8092", + "protoBuffer": "19445", + "flatBuffer": "19400", + }, + "presentationURL": "index.html", + "iconList": { + "icon": { + "mimetype": "image/png", + "height": "100", + "width": "100", + "depth": "32", + "url": "img/hyperion/ssdp_icon.png", + } + }, + "ssdp_usn": f"uuid:{TEST_SYSINFO_ID}", + "ssdp_ext": "", + "ssdp_server": "Raspbian GNU/Linux 10 (buster)/10 UPnP/1.0 Hyperion/2.0.0-alpha.8", +} + + +async def _create_mock_entry(hass: HomeAssistantType) -> MockConfigEntry: + """Add a test Hyperion entity to hass.""" + entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + entry_id=TEST_CONFIG_ENTRY_ID, + domain=DOMAIN, + unique_id=TEST_SYSINFO_ID, + title=TEST_TITLE, + data={ + "host": TEST_HOST, + "port": TEST_PORT, + "instance": TEST_INSTANCE, + }, + ) + entry.add_to_hass(hass) # type: ignore[no-untyped-call] + + # Setup + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +async def _init_flow( + hass: HomeAssistantType, + source: str = SOURCE_USER, + data: Optional[Dict[str, Any]] = None, +) -> Any: + """Initialize a flow.""" + data = data or {} + + return await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + + +async def _configure_flow( + hass: HomeAssistantType, result: Dict, user_input: Optional[Dict[str, Any]] = None +) -> Any: + """Provide input to a flow.""" + user_input = user_input or {} + + with patch( + "homeassistant.components.hyperion.async_setup", return_value=True + ), patch( + "homeassistant.components.hyperion.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + await hass.async_block_till_done() + return result + + +async def test_user_if_no_configuration(hass: HomeAssistantType) -> None: + """Check flow behavior when no configuration is present.""" + result = await _init_flow(hass) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["handler"] == DOMAIN + + +async def test_user_existing_id_abort(hass: HomeAssistantType) -> None: + """Verify a duplicate ID results in an abort.""" + result = await _init_flow(hass) + + await _create_mock_entry(hass) + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_client_errors(hass: HomeAssistantType) -> None: + """Verify correct behaviour with client errors.""" + result = await _init_flow(hass) + + client = create_mock_client() + + # Fail the connection. + client.async_client_connect = AsyncMock(return_value=False) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"]["base"] == "cannot_connect" + + # Fail the auth check call. + client.async_client_connect = AsyncMock(return_value=True) + client.async_is_auth_required = AsyncMock(return_value={"success": False}) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "auth_required_error" + + +async def test_user_confirm_cannot_connect(hass: HomeAssistantType) -> None: + """Test a failure to connect during confirmation.""" + + result = await _init_flow(hass) + + good_client = create_mock_client() + bad_client = create_mock_client() + bad_client.async_client_connect = AsyncMock(return_value=False) + + # Confirmation sync_client_connect fails. + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + side_effect=[good_client, bad_client], + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_user_confirm_id_error(hass: HomeAssistantType) -> None: + """Test a failure fetching the server id during confirmation.""" + result = await _init_flow(hass) + + client = create_mock_client() + client.async_sysinfo_id = AsyncMock(return_value=None) + + # Confirmation sync_client_connect fails. + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_id" + + +async def test_user_noauth_flow_success(hass: HomeAssistantType) -> None: + """Check a full flow without auth.""" + result = await _init_flow(hass) + + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["handler"] == DOMAIN + assert result["title"] == TEST_TITLE + assert result["data"] == { + **TEST_HOST_PORT, + } + + +async def test_user_auth_required(hass: HomeAssistantType) -> None: + """Verify correct behaviour when auth is required.""" + result = await _init_flow(hass) + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + +async def test_auth_static_token_auth_required_fail(hass: HomeAssistantType) -> None: + """Verify correct behaviour with a failed auth required call.""" + result = await _init_flow(hass) + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=None) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "auth_required_error" + + +async def test_auth_static_token_success(hass: HomeAssistantType) -> None: + """Test a successful flow with a static token.""" + result = await _init_flow(hass) + assert result["step_id"] == "user" + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["handler"] == DOMAIN + assert result["title"] == TEST_TITLE + assert result["data"] == { + **TEST_HOST_PORT, + CONF_TOKEN: TEST_TOKEN, + } + + +async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None: + """Test correct behavior with a bad static token.""" + result = await _init_flow(hass) + assert result["step_id"] == "user" + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + # Fail the login call. + client.async_login = AsyncMock( + return_value={"command": "authorize-login", "success": False, "tan": 0} + ) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"]["base"] == "invalid_access_token" + + +async def test_auth_create_token_approval_declined(hass: HomeAssistantType) -> None: + """Verify correct behaviour when a token request is declined.""" + result = await _init_flow(hass) + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ): + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "create_token" + assert result["description_placeholders"] == { + CONF_AUTH_ID: TEST_AUTH_ID, + } + + result = await _configure_flow(hass, result) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["step_id"] == "create_token_external" + + # The flow will be automatically advanced by the auth token response. + + result = await _configure_flow(hass, result) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "auth_new_token_not_granted_error" + + +async def test_auth_create_token_when_issued_token_fails( + hass: HomeAssistantType, +) -> None: + """Verify correct behaviour when a token is granted by fails to authenticate.""" + result = await _init_flow(hass) + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ): + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "create_token" + assert result["description_placeholders"] == { + CONF_AUTH_ID: TEST_AUTH_ID, + } + + result = await _configure_flow(hass, result) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["step_id"] == "create_token_external" + + # The flow will be automatically advanced by the auth token response. + + # Make the last verification fail. + client.async_client_connect = AsyncMock(return_value=False) + + result = await _configure_flow(hass, result) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_auth_create_token_success(hass: HomeAssistantType) -> None: + """Verify correct behaviour when a token is successfully created.""" + result = await _init_flow(hass) + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ): + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "create_token" + assert result["description_placeholders"] == { + CONF_AUTH_ID: TEST_AUTH_ID, + } + + result = await _configure_flow(hass, result) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["step_id"] == "create_token_external" + + # The flow will be automatically advanced by the auth token response. + result = await _configure_flow(hass, result) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["handler"] == DOMAIN + assert result["title"] == TEST_TITLE + assert result["data"] == { + **TEST_HOST_PORT, + CONF_TOKEN: TEST_TOKEN, + } + + +async def test_ssdp_success(hass: HomeAssistantType) -> None: + """Check an SSDP flow.""" + + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) + await hass.async_block_till_done() + + # Accept the confirmation. + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _configure_flow(hass, result) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["handler"] == DOMAIN + assert result["title"] == TEST_TITLE + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + } + + +async def test_ssdp_cannot_connect(hass: HomeAssistantType) -> None: + """Check an SSDP flow that cannot connect.""" + + client = create_mock_client() + client.async_client_connect = AsyncMock(return_value=False) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_ssdp_missing_serial(hass: HomeAssistantType) -> None: + """Check an SSDP flow where no id is provided.""" + + client = create_mock_client() + bad_data = {**TEST_SSDP_SERVICE_INFO} + del bad_data["serialNumber"] + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_id" + + +async def test_ssdp_failure_bad_port_json(hass: HomeAssistantType) -> None: + """Check an SSDP flow with bad json port.""" + + client = create_mock_client() + bad_data: Dict[str, Any] = {**TEST_SSDP_SERVICE_INFO} + bad_data["ports"]["jsonServer"] = "not_a_port" + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) + result = await _configure_flow(hass, result) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON + + +async def test_ssdp_failure_bad_port_ui(hass: HomeAssistantType) -> None: + """Check an SSDP flow with bad ui port.""" + + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + bad_data = {**TEST_SSDP_SERVICE_INFO} + bad_data["ssdp_location"] = f"http://{TEST_HOST}:not_a_port/description.xml" + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ): + result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) + + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "create_token" + + # Verify a working URL is used despite the bad port number + assert result["description_placeholders"] == { + CONF_AUTH_ID: TEST_AUTH_ID, + } + + +async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None: + """Check an SSDP flow where no id is provided.""" + + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result_1 = await _init_flow( + hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO + ) + result_2 = await _init_flow( + hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO + ) + await hass.async_block_till_done() + + assert result_1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_2["reason"] == "already_in_progress" + + +async def test_import_success(hass: HomeAssistantType) -> None: + """Check an import flow from the old-style YAML.""" + + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow( + hass, + source=SOURCE_IMPORT, + data={ + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + }, + ) + await hass.async_block_till_done() + + # No human interaction should be required. + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["handler"] == DOMAIN + assert result["title"] == TEST_TITLE + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + } + + +async def test_import_cannot_connect(hass: HomeAssistantType) -> None: + """Check an import flow that cannot connect.""" + + client = create_mock_client() + client.async_client_connect = AsyncMock(return_value=False) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow( + hass, + source=SOURCE_IMPORT, + data={ + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_options(hass: HomeAssistantType) -> None: + """Check an options flow.""" + + config_entry = add_test_config_entry(hass) + + client = create_mock_client() + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get(TEST_ENTITY_ID_1) is not None + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + new_priority = 1 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_PRIORITY: new_priority} + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_PRIORITY: new_priority} + + # Turn the light on and ensure the new priority is used. + client.async_send_set_color = AsyncMock(return_value=True) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, + blocking=True, + ) + assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 8250cc6c9c2..36b3684f736 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1,82 +1,306 @@ """Tests for the Hyperion integration.""" +import logging +from types import MappingProxyType +from typing import Any, Optional + from hyperion import const -from homeassistant.components.hyperion import light as hyperion_light +from homeassistant import setup +from homeassistant.components.hyperion import ( + get_hyperion_unique_id, + light as hyperion_light, +) +from homeassistant.components.hyperion.const import DOMAIN, TYPE_HYPERION_LIGHT from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, - DOMAIN, + DOMAIN as LIGHT_DOMAIN, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.setup import async_setup_component +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import AsyncMock, Mock, call, patch +from . import ( + TEST_CONFIG_ENTRY_OPTIONS, + TEST_ENTITY_ID_1, + TEST_ENTITY_ID_2, + TEST_ENTITY_ID_3, + TEST_HOST, + TEST_INSTANCE_1, + TEST_INSTANCE_2, + TEST_INSTANCE_3, + TEST_PORT, + TEST_PRIORITY, + TEST_SYSINFO_ID, + TEST_YAML_ENTITY_ID, + TEST_YAML_NAME, + add_test_config_entry, + create_mock_client, + setup_test_config_entry, +) -TEST_HOST = "test-hyperion-host" -TEST_PORT = const.DEFAULT_PORT -TEST_NAME = "test_hyperion_name" -TEST_PRIORITY = 128 -TEST_ENTITY_ID = f"{DOMAIN}.{TEST_NAME}" +from tests.async_mock import AsyncMock, call, patch # type: ignore[attr-defined] + +_LOGGER = logging.getLogger(__name__) -def create_mock_client(): - """Create a mock Hyperion client.""" - mock_client = Mock() - mock_client.async_client_connect = AsyncMock(return_value=True) - mock_client.adjustment = None - mock_client.effects = None - mock_client.id = "%s:%i" % (TEST_HOST, TEST_PORT) - return mock_client - - -def call_registered_callback(client, key, *args, **kwargs): +def _call_registered_callback( + client: AsyncMock, key: str, *args: Any, **kwargs: Any +) -> None: """Call a Hyperion entity callback that was registered with the client.""" - return client.set_callbacks.call_args[0][0][key](*args, **kwargs) + client.set_callbacks.call_args[0][0][key](*args, **kwargs) -async def setup_entity(hass, client=None): +async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None: """Add a test Hyperion entity to hass.""" client = client or create_mock_client() - with patch("hyperion.client.HyperionClient", return_value=client): - assert await async_setup_component( + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + assert await setup.async_setup_component( hass, - DOMAIN, + LIGHT_DOMAIN, { - DOMAIN: { + LIGHT_DOMAIN: { "platform": "hyperion", - "name": TEST_NAME, + "name": TEST_YAML_NAME, "host": TEST_HOST, - "port": const.DEFAULT_PORT, + "port": TEST_PORT, "priority": TEST_PRIORITY, } }, ) - await hass.async_block_till_done() + await hass.async_block_till_done() -async def test_setup_platform(hass): - """Test setting up the platform.""" +def _get_config_entry_from_unique_id( + hass: HomeAssistantType, unique_id: str +) -> Optional[ConfigEntry]: + for entry in hass.config_entries.async_entries(domain=DOMAIN): + if TEST_SYSINFO_ID == entry.unique_id: + return entry + return None + + +async def test_setup_yaml_already_converted(hass: HomeAssistantType) -> None: + """Test an already converted YAML style config.""" + # This tests "Possibility 1" from async_setup_platform() + + # Add a pre-existing config entry. + add_test_config_entry(hass) client = create_mock_client() - await setup_entity(hass, client=client) - assert hass.states.get(TEST_ENTITY_ID) is not None + await _setup_entity_yaml(hass, client=client) + + # Setup should be skipped for the YAML config as there is a pre-existing config + # entry. + assert hass.states.get(TEST_YAML_ENTITY_ID) is None -async def test_setup_platform_not_ready(hass): - """Test the platform not being ready.""" +async def test_setup_yaml_old_style_unique_id(hass: HomeAssistantType) -> None: + """Test an already converted YAML style config.""" + # This tests "Possibility 2" from async_setup_platform() + old_unique_id = f"{TEST_HOST}:{TEST_PORT}-0" + + # Add a pre-existing registry entry. + registry = await async_get_registry(hass) + registry.async_get_or_create( + domain=LIGHT_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + suggested_object_id=TEST_YAML_NAME, + ) + + client = create_mock_client() + await _setup_entity_yaml(hass, client=client) + + # The entity should have been created with the same entity_id. + assert hass.states.get(TEST_YAML_ENTITY_ID) is not None + + # The unique_id should have been updated in the registry (rather than the one + # specified above). + assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id( + TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT + ) + assert registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, old_unique_id) is None + + # There should be a config entry with the correct server unique_id. + entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) + assert entry + assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS) + + +async def test_setup_yaml_new_style_unique_id_wo_config( + hass: HomeAssistantType, +) -> None: + """Test an a new unique_id without a config entry.""" + # Note: This casde should not happen in the wild, as no released version of Home + # Assistant should this combination, but verify correct behavior for defense in + # depth. + + new_unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT) + entity_id_to_preserve = "light.magic_entity" + + # Add a pre-existing registry entry. + registry = await async_get_registry(hass) + registry.async_get_or_create( + domain=LIGHT_DOMAIN, + platform=DOMAIN, + unique_id=new_unique_id, + suggested_object_id=entity_id_to_preserve.split(".")[1], + ) + + client = create_mock_client() + await _setup_entity_yaml(hass, client=client) + + # The entity should have been created with the same entity_id. + assert hass.states.get(entity_id_to_preserve) is not None + + # The unique_id should have been updated in the registry (rather than the one + # specified above). + assert registry.async_get(entity_id_to_preserve).unique_id == new_unique_id + + # There should be a config entry with the correct server unique_id. + entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) + assert entry + assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS) + + +async def test_setup_yaml_no_registry_entity(hass: HomeAssistantType) -> None: + """Test an already converted YAML style config.""" + # This tests "Possibility 3" from async_setup_platform() + + registry = await async_get_registry(hass) + + # Add a pre-existing config entry. + client = create_mock_client() + await _setup_entity_yaml(hass, client=client) + + # The entity should have been created with the same entity_id. + assert hass.states.get(TEST_YAML_ENTITY_ID) is not None + + # The unique_id should have been updated in the registry (rather than the one + # specified above). + assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id( + TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT + ) + + # There should be a config entry with the correct server unique_id. + entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) + assert entry + assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS) + + +async def test_setup_yaml_not_ready(hass: HomeAssistantType) -> None: + """Test the component not being ready.""" client = create_mock_client() client.async_client_connect = AsyncMock(return_value=False) - - await setup_entity(hass, client=client) - assert hass.states.get(TEST_ENTITY_ID) is None + await _setup_entity_yaml(hass, client=client) + assert hass.states.get(TEST_YAML_ENTITY_ID) is None -async def test_light_basic_properies(hass): +async def test_setup_config_entry(hass: HomeAssistantType) -> None: + """Test setting up the component via config entries.""" + await setup_test_config_entry(hass, hyperion_client=create_mock_client()) + assert hass.states.get(TEST_ENTITY_ID_1) is not None + + +async def test_setup_config_entry_not_ready(hass: HomeAssistantType) -> None: + """Test the component not being ready.""" + client = create_mock_client() + client.async_client_connect = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is None + + +async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None: + """Test dynamic changes in the omstamce configuration.""" + config_entry = add_test_config_entry(hass) + + master_client = create_mock_client() + master_client.instances = [TEST_INSTANCE_1, TEST_INSTANCE_2] + + entity_client = create_mock_client() + entity_client.instances = master_client.instances + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + side_effect=[master_client, entity_client, entity_client], + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID_1) is not None + assert hass.states.get(TEST_ENTITY_ID_2) is not None + + # Inject a new instances update (remove instance 1, add instance 3) + assert master_client.set_callbacks.called + instance_callback = master_client.set_callbacks.call_args[0][0][ + f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}" + ] + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=entity_client, + ): + instance_callback( + { + const.KEY_SUCCESS: True, + const.KEY_DATA: [TEST_INSTANCE_2, TEST_INSTANCE_3], + } + ) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID_1) is None + assert hass.states.get(TEST_ENTITY_ID_2) is not None + assert hass.states.get(TEST_ENTITY_ID_3) is not None + + # Inject a new instances update (re-add instance 1, but not running) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=entity_client, + ): + instance_callback( + { + const.KEY_SUCCESS: True, + const.KEY_DATA: [ + {**TEST_INSTANCE_1, "running": False}, + TEST_INSTANCE_2, + TEST_INSTANCE_3, + ], + } + ) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID_1) is None + assert hass.states.get(TEST_ENTITY_ID_2) is not None + assert hass.states.get(TEST_ENTITY_ID_3) is not None + + # Inject a new instances update (re-add instance 1, running) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=entity_client, + ): + instance_callback( + { + const.KEY_SUCCESS: True, + const.KEY_DATA: [TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3], + } + ) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID_1) is not None + assert hass.states.get(TEST_ENTITY_ID_2) is not None + assert hass.states.get(TEST_ENTITY_ID_3) is not None + + +async def test_light_basic_properies(hass: HomeAssistantType) -> None: """Test the basic properties.""" client = create_mock_client() - await setup_entity(hass, client=client) + await setup_test_config_entry(hass, hyperion_client=client) - entity_state = hass.states.get(TEST_ENTITY_ID) + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.state == "on" assert entity_state.attributes["brightness"] == 255 assert entity_state.attributes["hs_color"] == (0.0, 0.0) @@ -91,15 +315,15 @@ async def test_light_basic_properies(hass): ) -async def test_light_async_turn_on(hass): +async def test_light_async_turn_on(hass: HomeAssistantType) -> None: """Test turning the light on.""" client = create_mock_client() - await setup_entity(hass, client=client) + await setup_test_config_entry(hass, hyperion_client=client) # On (=), 100% (=), solid (=), [255,255,255] (=) client.async_send_set_color = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True ) assert client.async_send_set_color.call_args == call( @@ -116,9 +340,9 @@ async def test_light_async_turn_on(hass): client.async_send_set_color = AsyncMock(return_value=True) client.async_send_set_adjustment = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, + LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_BRIGHTNESS: brightness}, blocking=True, ) @@ -135,8 +359,9 @@ async def test_light_async_turn_on(hass): # Simulate a state callback from Hyperion. client.adjustment = [{const.KEY_BRIGHTNESS: 50}] - call_registered_callback(client, "adjustment-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.state == "on" assert entity_state.attributes["brightness"] == brightness @@ -144,9 +369,9 @@ async def test_light_async_turn_on(hass): hs_color = (180.0, 100.0) client.async_send_set_color = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, + LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HS_COLOR: hs_color}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_HS_COLOR: hs_color}, blocking=True, ) @@ -164,8 +389,9 @@ async def test_light_async_turn_on(hass): const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)}, } - call_registered_callback(client, "priorities-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["hs_color"] == hs_color assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB @@ -175,9 +401,9 @@ async def test_light_async_turn_on(hass): client.async_send_set_adjustment = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, + LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_BRIGHTNESS: brightness}, blocking=True, ) @@ -192,8 +418,9 @@ async def test_light_async_turn_on(hass): } ) client.adjustment = [{const.KEY_BRIGHTNESS: 100}] - call_registered_callback(client, "adjustment-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["brightness"] == brightness # On (=), 100% (=), V4L (!), [0,255,255] (=) @@ -201,9 +428,9 @@ async def test_light_async_turn_on(hass): client.async_send_clear = AsyncMock(return_value=True) client.async_send_set_component = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, + LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: effect}, blocking=True, ) @@ -237,8 +464,9 @@ async def test_light_async_turn_on(hass): ), ] client.visible_priority = {const.KEY_COMPONENTID: effect} - call_registered_callback(client, "priorities-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE assert entity_state.attributes["effect"] == effect @@ -248,9 +476,9 @@ async def test_light_async_turn_on(hass): client.async_send_set_effect = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, + LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: effect}, blocking=True, ) @@ -268,33 +496,37 @@ async def test_light_async_turn_on(hass): const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT, const.KEY_OWNER: effect, } - call_registered_callback(client, "priorities-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT assert entity_state.attributes["effect"] == effect # No calls if disconnected. client.has_loaded_state = False - call_registered_callback(client, "client-update", {"loaded-state": False}) + _call_registered_callback(client, "client-update", {"loaded-state": False}) client.async_send_clear = AsyncMock(return_value=True) client.async_send_set_effect = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True ) assert not client.async_send_clear.called assert not client.async_send_set_effect.called -async def test_light_async_turn_off(hass): +async def test_light_async_turn_off(hass: HomeAssistantType) -> None: """Test turning the light off.""" client = create_mock_client() - await setup_entity(hass, client=client) + await setup_test_config_entry(hass, hyperion_client=client) client.async_send_set_component = AsyncMock(return_value=True) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, + blocking=True, ) assert client.async_send_set_component.call_args == call( @@ -309,50 +541,60 @@ async def test_light_async_turn_off(hass): # No calls if no state loaded. client.has_loaded_state = False client.async_send_set_component = AsyncMock(return_value=True) - call_registered_callback(client, "client-update", {"loaded-state": False}) + _call_registered_callback(client, "client-update", {"loaded-state": False}) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, + blocking=True, ) assert not client.async_send_set_component.called -async def test_light_async_updates_from_hyperion_client(hass): +async def test_light_async_updates_from_hyperion_client( + hass: HomeAssistantType, +) -> None: """Test receiving a variety of Hyperion client callbacks.""" client = create_mock_client() - await setup_entity(hass, client=client) + await setup_test_config_entry(hass, hyperion_client=client) # Bright change gets accepted. brightness = 10 client.adjustment = [{const.KEY_BRIGHTNESS: brightness}] - call_registered_callback(client, "adjustment-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) # Broken brightness value is ignored. bad_brightness = -200 client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}] - call_registered_callback(client, "adjustment-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) # Update components. client.is_on.return_value = True - call_registered_callback(client, "components-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "components-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.state == "on" client.is_on.return_value = False - call_registered_callback(client, "components-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "components-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.state == "off" # Update priorities (V4L) client.is_on.return_value = True client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L} - call_registered_callback(client, "priorities-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE assert entity_state.attributes["hs_color"] == (0.0, 0.0) assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L @@ -364,8 +606,9 @@ async def test_light_async_updates_from_hyperion_client(hass): const.KEY_OWNER: effect, } - call_registered_callback(client, "priorities-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["effect"] == effect assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT assert entity_state.attributes["hs_color"] == (0.0, 0.0) @@ -377,8 +620,9 @@ async def test_light_async_updates_from_hyperion_client(hass): const.KEY_VALUE: {const.KEY_RGB: rgb}, } - call_registered_callback(client, "priorities-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB assert entity_state.attributes["hs_color"] == (180.0, 100.0) @@ -386,8 +630,9 @@ async def test_light_async_updates_from_hyperion_client(hass): # Update effect list effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] client.effects = effects - call_registered_callback(client, "effects-update") - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "effects-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["effect_list"] == [ effect[const.KEY_NAME] for effect in effects ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID] @@ -396,18 +641,20 @@ async def test_light_async_updates_from_hyperion_client(hass): # Turn on late, check state, disconnect, ensure it cannot be turned off. client.has_loaded_state = False - call_registered_callback(client, "client-update", {"loaded-state": False}) - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "client-update", {"loaded-state": False}) + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.state == "unavailable" # Update connection status (e.g. re-connection) client.has_loaded_state = True - call_registered_callback(client, "client-update", {"loaded-state": True}) - entity_state = hass.states.get(TEST_ENTITY_ID) + _call_registered_callback(client, "client-update", {"loaded-state": True}) + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.state == "on" -async def test_full_state_loaded_on_start(hass): +async def test_full_state_loaded_on_start(hass: HomeAssistantType) -> None: """Test receiving a variety of Hyperion client callbacks.""" client = create_mock_client() @@ -420,11 +667,43 @@ async def test_full_state_loaded_on_start(hass): } client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] - await setup_entity(hass, client=client) - - entity_state = hass.states.get(TEST_ENTITY_ID) + await setup_test_config_entry(hass, hyperion_client=client) + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB assert entity_state.attributes["hs_color"] == (180.0, 100.0) + + +async def test_unload_entry(hass: HomeAssistantType) -> None: + """Test unload.""" + client = create_mock_client() + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is not None + assert client.async_client_connect.called + assert not client.async_client_disconnect.called + entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) + assert entry + + await hass.config_entries.async_unload(entry.entry_id) + assert client.async_client_disconnect.call_count == 2 + + +async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: + """Test warning on old version.""" + client = create_mock_client() + client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.7") + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is not None + assert "Please consider upgrading" in caplog.text + + +async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: + """Test no warning on acceptable version.""" + client = create_mock_client() + client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.9") + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is not None + assert "Please consider upgrading" not in caplog.text From 3f5d7e85c3dee76c0a5ebb02b478f40feff7ba5d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 30 Nov 2020 18:59:15 +0100 Subject: [PATCH 307/430] Add show progress to ozw config flow (#43310) --- homeassistant/components/ozw/config_flow.py | 30 ++++++++++++++++--- homeassistant/components/ozw/strings.json | 6 ++++ .../components/ozw/translations/en.json | 6 ++++ tests/components/ozw/test_config_flow.py | 17 +++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 1c0ccdefa70..9ecbcbe76bb 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -36,6 +36,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = False # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False + self.install_task = None async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -93,16 +94,27 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_install_addon() - async def async_step_install_addon(self): + async def async_step_install_addon(self, user_input=None): """Install OpenZWave add-on.""" + if not self.install_task: + self.install_task = self.hass.async_create_task(self._async_install_addon()) + return self.async_show_progress( + step_id="install_addon", progress_action="install_addon" + ) + try: - await self.hass.components.hassio.async_install_addon("core_zwave") + await self.install_task except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to install OpenZWave add-on: %s", err) - return self.async_abort(reason="addon_install_failed") + return self.async_show_progress_done(next_step_id="install_failed") + self.integration_created_addon = True - return await self.async_step_start_addon() + return self.async_show_progress_done(next_step_id="start_addon") + + async def async_step_install_failed(self, user_input=None): + """Add-on installation failed.""" + return self.async_abort(reason="addon_install_failed") async def async_step_start_addon(self, user_input=None): """Ask for config and start OpenZWave add-on.""" @@ -181,3 +193,13 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to set OpenZWave add-on config: %s", err) raise AbortFlow("addon_set_config_failed") from err + + async def _async_install_addon(self): + """Install the OpenZWave add-on.""" + try: + await self.hass.components.hassio.async_install_addon("core_zwave") + finally: + # Continue the flow after show progress when the task is done. + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json index 52317b3d6a8..f006f0663f0 100644 --- a/homeassistant/components/ozw/strings.json +++ b/homeassistant/components/ozw/strings.json @@ -6,6 +6,9 @@ "description": "Do you want to use the OpenZWave Supervisor add-on?", "data": {"use_addon": "Use the OpenZWave Supervisor add-on"} }, + "install_addon": { + "title": "The OpenZWave add-on installation has started" + }, "start_addon": { "title": "Enter the OpenZWave add-on configuration", "data": {"usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key"} @@ -20,6 +23,9 @@ }, "error": { "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." + }, + "progress": { + "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." } } } diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json index e028e1923ae..e21b1819883 100644 --- a/homeassistant/components/ozw/translations/en.json +++ b/homeassistant/components/ozw/translations/en.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." }, + "progress": { + "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." + }, "step": { + "install_addon": { + "title": "The OpenZWave add-on installation has started" + }, "on_supervisor": { "data": { "use_addon": "Use the OpenZWave Supervisor add-on" diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 7561244999d..7a2b89967c5 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -305,6 +305,16 @@ async def test_addon_not_installed( result["flow_id"], {"use_addon": True} ) + assert result["type"] == "progress" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + with patch( "homeassistant.components.ozw.async_setup", return_value=True ) as mock_setup, patch( @@ -342,5 +352,12 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ result["flow_id"], {"use_addon": True} ) + assert result["type"] == "progress" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "abort" assert result["reason"] == "addon_install_failed" From 434cec7a88634148b328681da6a08db774c949e7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 30 Nov 2020 19:38:39 +0100 Subject: [PATCH 308/430] Pin pip < 20.3 (#43771) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4e8cba12564..56b181aa02a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U "pip<20.3" setuptools pip install -r requirements.txt -r requirements_test.txt - name: Restore pre-commit environment from cache id: cache-precommit @@ -611,7 +611,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools wheel + pip install -U "pip<20.3" setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt pip install -e . diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80f74e708b6..e19817bb2c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ jinja2>=2.11.2 netdisco==2.8.2 paho-mqtt==1.5.1 pillow==7.2.0 -pip>=8.0.3 +pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.1 pyyaml==5.3.1 diff --git a/requirements.txt b/requirements.txt index 3a376b6e7cc..ece1877ea75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 PyJWT==1.7.1 cryptography==3.2 -pip>=8.0.3 +pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.1 pyyaml==5.3.1 diff --git a/setup.py b/setup.py index 885d6e192d6..d5d133d4a3a 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ REQUIRES = [ "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.2", - "pip>=8.0.3", + "pip>=8.0.3,<20.3", "python-slugify==4.0.1", "pytz>=2020.1", "pyyaml==5.3.1", From ba4d630470a8bb699fd919719be915b4dbc02c42 Mon Sep 17 00:00:00 2001 From: Willem-Jan Date: Mon, 30 Nov 2020 20:13:16 +0100 Subject: [PATCH 309/430] Add authentication support to bsblan (#42306) --- homeassistant/components/bsblan/__init__.py | 4 +- .../components/bsblan/config_flow.py | 24 +++++- homeassistant/components/bsblan/strings.json | 4 +- tests/components/bsblan/__init__.py | 44 ++++++++++- tests/components/bsblan/test_config_flow.py | 73 ++++++++++++++++++- tests/components/bsblan/test_init.py | 47 ++++++++++++ 6 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 tests/components/bsblan/test_init.py diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 9f4bb38e315..bab4af29422 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -5,7 +5,7 @@ from bsblan import BSBLan, BSBLanConnectionError from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -29,6 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_HOST], passkey=entry.data[CONF_PASSKEY], port=entry.data[CONF_PORT], + username=entry.data.get(CONF_USERNAME), + password=entry.data.get(CONF_PASSWORD), session=session, ) diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index faca81bb6a7..dee04e6ef85 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -6,7 +6,7 @@ from bsblan import BSBLan, BSBLanError, 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.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -37,6 +37,8 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): host=user_input[CONF_HOST], port=user_input[CONF_PORT], passkey=user_input.get(CONF_PASSKEY), + username=user_input.get(CONF_USERNAME), + password=user_input.get(CONF_PASSWORD), ) except BSBLanError: return self._show_setup_form({"base": "cannot_connect"}) @@ -52,6 +54,8 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PORT: user_input[CONF_PORT], CONF_PASSKEY: user_input.get(CONF_PASSKEY), CONF_DEVICE_IDENT: info.device_identification, + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_PASSWORD: user_input.get(CONF_PASSWORD), }, ) @@ -64,16 +68,30 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): vol.Required(CONF_HOST): str, vol.Optional(CONF_PORT, default=80): int, vol.Optional(CONF_PASSKEY): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, } ), errors=errors or {}, ) async def _get_bsblan_info( - self, host: str, passkey: Optional[str], port: int + self, + host: str, + username: Optional[str], + password: Optional[str], + passkey: Optional[str], + port: int, ) -> Info: """Get device information from an BSBLan device.""" session = async_get_clientsession(self.hass) _LOGGER.debug("request bsblan.info:") - bsblan = BSBLan(host, passkey=passkey, port=port, session=session) + bsblan = BSBLan( + host, + username=username, + password=password, + passkey=passkey, + port=port, + session=session, + ) return await bsblan.info() diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index d9510808fc1..0bb084fb20d 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -8,7 +8,9 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", - "passkey": "Passkey string" + "passkey": "Passkey string", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py index 511b566ce41..f2e88d97ba2 100644 --- a/tests/components/bsblan/__init__.py +++ b/tests/components/bsblan/__init__.py @@ -5,7 +5,13 @@ from homeassistant.components.bsblan.const import ( CONF_PASSKEY, DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONTENT_TYPE_JSON, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -26,6 +32,42 @@ async def init_integration( headers={"Content-Type": CONTENT_TYPE_JSON}, ) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="RVS21.831F/127", + data={ + CONF_HOST: "example.local", + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + CONF_PASSKEY: "1234", + CONF_PORT: 80, + CONF_DEVICE_IDENT: "RVS21.831F/127", + }, + ) + + 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 + + +async def init_integration_without_auth( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the BSBLan integration in Home Assistant.""" + + aioclient_mock.post( + "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", + params={"Parameter": "6224,6225,6226"}, + text=load_fixture("bsblan/info.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + entry = MockConfigEntry( domain=DOMAIN, unique_id="RVS21.831F/127", diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 4c04db012ba..38485fb7959 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -5,7 +5,13 @@ from homeassistant import data_entry_flow from homeassistant.components.bsblan import config_flow from homeassistant.components.bsblan.const import CONF_DEVICE_IDENT, CONF_PASSKEY from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONTENT_TYPE_JSON, +) from homeassistant.core import HomeAssistant from . import init_integration @@ -37,7 +43,13 @@ async def test_connection_error( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_USER}, - data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80}, + data={ + CONF_HOST: "example.local", + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + CONF_PASSKEY: "1234", + CONF_PORT: 80, + }, ) assert result["errors"] == {"base": "cannot_connect"} @@ -54,7 +66,13 @@ async def test_user_device_exists_abort( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_USER}, - data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80}, + data={ + CONF_HOST: "example.local", + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + CONF_PASSKEY: "1234", + CONF_PORT: 80, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -80,10 +98,18 @@ async def test_full_user_flow_implementation( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80}, + user_input={ + CONF_HOST: "example.local", + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + CONF_PASSKEY: "1234", + CONF_PORT: 80, + }, ) assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_USERNAME] == "nobody" + assert result["data"][CONF_PASSWORD] == "qwerty" assert result["data"][CONF_PASSKEY] == "1234" assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" @@ -92,3 +118,42 @@ async def test_full_user_flow_implementation( entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" + + +async def test_full_user_flow_implementation_without_auth( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "http://example2.local:80/JQ?Parameter=6224,6225,6226", + text=load_fixture("bsblan/info.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "example2.local", + CONF_PORT: 80, + }, + ) + + assert result["data"][CONF_HOST] == "example2.local" + assert result["data"][CONF_USERNAME] is None + assert result["data"][CONF_PASSWORD] is None + assert result["data"][CONF_PASSKEY] is None + assert result["data"][CONF_PORT] == 80 + assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" + assert result["title"] == "RVS21.831F/127" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entries = hass.config_entries.async_entries(config_flow.DOMAIN) + assert entries[0].unique_id == "RVS21.831F/127" diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py new file mode 100644 index 00000000000..b6096ced0ac --- /dev/null +++ b/tests/components/bsblan/test_init.py @@ -0,0 +1,47 @@ +"""Tests for the BSBLan integration.""" +import aiohttp + +from homeassistant.components.bsblan.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant + +from tests.components.bsblan import init_integration, init_integration_without_auth +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the BSBLan configuration entry not ready.""" + aioclient_mock.post( + "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", + 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 BSBLan 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_config_entry_no_authentication( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the BSBLan configuration entry not ready.""" + aioclient_mock.post( + "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", + exc=aiohttp.ClientError, + ) + + entry = await init_integration_without_auth(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY From 2f7359071412a4ba34a854334cc748742a45b630 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 30 Nov 2020 20:40:31 +0100 Subject: [PATCH 310/430] Increase Supervisor add-on helper timeout (#43778) --- homeassistant/components/hassio/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6484c85b95e..1e05321b60f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -159,7 +159,7 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict: """ hassio = hass.data[DOMAIN] command = f"/addons/{slug}/uninstall" - return await hassio.send_command(command) + return await hassio.send_command(command, timeout=60) @bind_hass @@ -183,7 +183,7 @@ async def async_stop_addon(hass: HomeAssistantType, slug: str) -> dict: """ hassio = hass.data[DOMAIN] command = f"/addons/{slug}/stop" - return await hassio.send_command(command) + return await hassio.send_command(command, timeout=60) @bind_hass From a5c79a1f84c66760d3630deb4fd0920e53da1982 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Nov 2020 23:35:24 +0100 Subject: [PATCH 311/430] Add support for device class in target selector (#43768) --- homeassistant/helpers/selector.py | 2 +- tests/helpers/test_selector.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index d81fa396c0b..b4a90212bce 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -120,4 +120,4 @@ class TargetSelector(Selector): Value should follow cv.ENTITY_SERVICE_FIELDS format. """ - CONFIG_SCHEMA = vol.Schema({"entity": {"domain": str}}) + CONFIG_SCHEMA = vol.Schema({"entity": {"domain": str, "device_class": str}}) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 2c2034147a1..531d36e4f50 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -125,6 +125,7 @@ def test_time_selector_schema(schema): {}, {"entity": {}}, {"entity": {"domain": "light"}}, + {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, ), ) def test_target_selector_schema(schema): From d18a33020b916c54ae821748a786588477c365a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Dec 2020 01:01:22 +0100 Subject: [PATCH 312/430] Fix MQTT birth message deadlock (#43790) --- homeassistant/components/mqtt/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 800d9e4eb73..05d7d0adb86 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -944,7 +944,9 @@ class MQTT: ) birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) - self.hass.add_job(publish_birth_message(birth_message)) + asyncio.run_coroutine_threadsafe( + publish_birth_message(birth_message), self.hass.loop + ) def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: """Message received callback.""" From cf9598fe4f034820297b4cff019eb43850ad4e74 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 1 Dec 2020 00:03:00 +0000 Subject: [PATCH 313/430] [ci skip] Translation update --- .../components/abode/translations/lb.json | 16 +++++- .../components/abode/translations/no.json | 17 +++++- .../alarm_control_panel/translations/et.json | 6 +-- .../components/aurora/translations/lb.json | 25 +++++++++ .../components/bsblan/translations/en.json | 4 +- .../device_tracker/translations/lb.json | 4 ++ .../dialogflow/translations/lb.json | 3 +- .../components/epson/translations/lb.json | 16 ++++++ .../fireservicerota/translations/lb.json | 29 +++++++++++ .../components/geofency/translations/lb.json | 3 +- .../components/gpslogger/translations/lb.json | 3 +- .../components/hassio/translations/lb.json | 15 ++++++ .../homeassistant/translations/lb.json | 21 ++++++++ .../components/hyperion/translations/en.json | 52 +++++++++++++++++++ .../components/hyperion/translations/et.json | 52 +++++++++++++++++++ .../components/ifttt/translations/lb.json | 3 +- .../components/ipma/translations/en.json | 5 ++ .../components/ipma/translations/et.json | 5 ++ .../components/kodi/translations/lb.json | 1 + .../components/locative/translations/lb.json | 3 +- .../components/lovelace/translations/lb.json | 10 ++++ .../components/mailgun/translations/lb.json | 3 +- .../motion_blinds/translations/lb.json | 17 ++++++ .../components/nest/translations/en.json | 4 +- .../components/nest/translations/es.json | 8 +++ .../components/nest/translations/et.json | 8 +++ .../components/nest/translations/lb.json | 8 +++ .../components/nest/translations/ru.json | 8 +++ .../components/nest/translations/zh-Hant.json | 8 +++ .../components/ozw/translations/et.json | 6 +++ .../components/spotify/translations/no.json | 5 ++ .../components/zha/translations/ru.json | 2 +- 32 files changed, 355 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/aurora/translations/lb.json create mode 100644 homeassistant/components/epson/translations/lb.json create mode 100644 homeassistant/components/fireservicerota/translations/lb.json create mode 100644 homeassistant/components/homeassistant/translations/lb.json create mode 100644 homeassistant/components/hyperion/translations/en.json create mode 100644 homeassistant/components/hyperion/translations/et.json create mode 100644 homeassistant/components/lovelace/translations/lb.json create mode 100644 homeassistant/components/motion_blinds/translations/lb.json diff --git a/homeassistant/components/abode/translations/lb.json b/homeassistant/components/abode/translations/lb.json index 8a6d671b7f0..127281176be 100644 --- a/homeassistant/components/abode/translations/lb.json +++ b/homeassistant/components/abode/translations/lb.json @@ -1,13 +1,27 @@ { "config": { "abort": { + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "error": { "cannot_connect": "Feeler beim verbannen", - "invalid_auth": "Ong\u00eblteg Authentifikatioun" + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "invalid_mfa_code": "Ong\u00ebltege MFA Code" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA code (6 Zifferen)" + }, + "title": "G\u00ebff dain MFA code fir Abode un" + }, + "reauth_confirm": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + } + }, "user": { "data": { "password": "Passwuert", diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json index cda0a6aa221..c215ec7dae9 100644 --- a/homeassistant/components/abode/translations/no.json +++ b/homeassistant/components/abode/translations/no.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Reautentisering var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning" + "invalid_auth": "Ugyldig godkjenning", + "invalid_mfa_code": "Ugyldig MFA-kode" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-kode (6-sifre)" + }, + "title": "Skriv inn din MFA-kode for Abode" + }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "title": "Fyll ut innloggingsinformasjonen for Abode" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant/components/alarm_control_panel/translations/et.json index 76b0c845d01..cc4bb6f1ea3 100644 --- a/homeassistant/components/alarm_control_panel/translations/et.json +++ b/homeassistant/components/alarm_control_panel/translations/et.json @@ -15,9 +15,9 @@ "is_triggered": "{entity_name} on h\u00e4iret andnud" }, "trigger_type": { - "armed_away": "{entity_name} valvestatus", - "armed_home": "{entity_name} valvestatus kodure\u017eiimis", - "armed_night": "{entity_name} valvestatus \u00f6\u00f6re\u017eiimis", + "armed_away": "{entity_name} valvestati", + "armed_home": "{entity_name} valvestati kodure\u017eiimis", + "armed_night": "{entity_name} valvestati \u00f6\u00f6re\u017eiimis", "disarmed": "{entity_name} v\u00f5eti valvest maha", "triggered": "{entity_name} andis h\u00e4iret" } diff --git a/homeassistant/components/aurora/translations/lb.json b/homeassistant/components/aurora/translations/lb.json new file mode 100644 index 00000000000..2fb42afcab3 --- /dev/null +++ b/homeassistant/components/aurora/translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "latitude": "L\u00e4ngregraad", + "longitude": "Breedegrad", + "name": "Numm" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Grenzw\u00e4ert (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/en.json b/homeassistant/components/bsblan/translations/en.json index b0773c5bb5e..4aa8b881cb4 100644 --- a/homeassistant/components/bsblan/translations/en.json +++ b/homeassistant/components/bsblan/translations/en.json @@ -12,7 +12,9 @@ "data": { "host": "Host", "passkey": "Passkey string", - "port": "Port" + "password": "Password", + "port": "Port", + "username": "Username" }, "description": "Set up you BSB-Lan device to integrate with Home Assistant.", "title": "Connect to the BSB-Lan device" diff --git a/homeassistant/components/device_tracker/translations/lb.json b/homeassistant/components/device_tracker/translations/lb.json index 88d1b40b7ba..6b38b297b94 100644 --- a/homeassistant/components/device_tracker/translations/lb.json +++ b/homeassistant/components/device_tracker/translations/lb.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} ass doheem", "is_not_home": "{entity_name} ass net doheem" + }, + "trigger_type": { + "enters": "{entity_name} k\u00ebnnt an eng Zone", + "leaves": "{entity_name} verl\u00e9isst eng Zone" } }, "state": { diff --git a/homeassistant/components/dialogflow/translations/lb.json b/homeassistant/components/dialogflow/translations/lb.json index 85d5d30128b..15730baccb6 100644 --- a/homeassistant/components/dialogflow/translations/lb.json +++ b/homeassistant/components/dialogflow/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss [Webhook Integratioun mat Dialogflow]({dialogflow_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." diff --git a/homeassistant/components/epson/translations/lb.json b/homeassistant/components/epson/translations/lb.json new file mode 100644 index 00000000000..e8d9f52998f --- /dev/null +++ b/homeassistant/components/epson/translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Numm", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/lb.json b/homeassistant/components/fireservicerota/translations/lb.json new file mode 100644 index 00000000000..9f852c8fdfb --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert", + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" + }, + "error": { + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "step": { + "reauth": { + "data": { + "password": "Passwuert" + }, + "description": "Acc\u00e8s Jetons sin ong\u00eblteg, verbann dech fir se n\u00e9i z'erstellen" + }, + "user": { + "data": { + "password": "Passwuert", + "url": "Webs\u00e4it", + "username": "Benotzernumm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/lb.json b/homeassistant/components/geofency/translations/lb.json index 1e7b20d8423..b8b6da7707b 100644 --- a/homeassistant/components/geofency/translations/lb.json +++ b/homeassistant/components/geofency/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Geofency ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." diff --git a/homeassistant/components/gpslogger/translations/lb.json b/homeassistant/components/gpslogger/translations/lb.json index b3b137544f3..25f073ebd8c 100644 --- a/homeassistant/components/gpslogger/translations/lb.json +++ b/homeassistant/components/gpslogger/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am GPSLogger ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." diff --git a/homeassistant/components/hassio/translations/lb.json b/homeassistant/components/hassio/translations/lb.json index 981cb51c83a..eefcb6110a1 100644 --- a/homeassistant/components/hassio/translations/lb.json +++ b/homeassistant/components/hassio/translations/lb.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "disk_total": "Disk Toal", + "disk_used": "Disk benotzt", + "docker_version": "Docker Versioun", + "healthy": "Gesond", + "host_os": "Host Betribssystem", + "installed_addons": "Install\u00e9iert Add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor Versioun", + "supported": "\u00cbnnerst\u00ebtzt", + "update_channel": "Aktualis\u00e9ierungs Kanal", + "version_api": "API Versioun" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/lb.json b/homeassistant/components/homeassistant/translations/lb.json new file mode 100644 index 00000000000..07cfe8c4c83 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/lb.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "CPU Architektur", + "chassis": "Chassis", + "dev": "Entw\u00e9cklung", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor API", + "host_os": "Home Assistant OS", + "installation_type": "Typ vun Installatioun", + "os_name": "Betribssystem Famille", + "os_version": "Betribssystem Versioun", + "python_version": "Python Versioun", + "supervisor": "Supervisor", + "timezone": "Z\u00e4itzon", + "version": "Versioun", + "virtualenv": "Virtuellen Environnement" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/en.json b/homeassistant/components/hyperion/translations/en.json new file mode 100644 index 00000000000..c4c4f512d6f --- /dev/null +++ b/homeassistant/components/hyperion/translations/en.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "already_in_progress": "Configuration flow is already in progress", + "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI", + "auth_new_token_not_work_error": "Failed to authenticate using newly created token", + "auth_required_error": "Failed to determine if authorization is required", + "cannot_connect": "Failed to connect", + "no_id": "The Hyperion Ambilight instance did not report its id" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_access_token": "Invalid access token" + }, + "step": { + "auth": { + "data": { + "create_token": "Automatically create new token", + "token": "Or provide pre-existing token" + }, + "description": "Configure authorization to your Hyperion Ambilight server" + }, + "confirm": { + "description": "Do you want to add this Hyperion Ambilight to Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Confirm addition of Hyperion Ambilight service" + }, + "create_token": { + "description": "Choose **Submit** below to request a new authentication token. You will be redirected to the Hyperion UI to approve the request. Please verify the shown id is \"{auth_id}\"", + "title": "Automatically create new authentication token" + }, + "create_token_external": { + "title": "Accept new token in Hyperion UI" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion priority to use for colors and effects" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/et.json b/homeassistant/components/hyperion/translations/et.json new file mode 100644 index 00000000000..e8d6232236b --- /dev/null +++ b/homeassistant/components/hyperion/translations/et.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "auth_new_token_not_granted_error": "\u00c4sja loodud juurdep\u00e4\u00e4sut\u00f5end ei ole Hyperioni oma", + "auth_new_token_not_work_error": "Loodud juurdep\u00e4\u00e4sut\u00f5endiga autentimine nurjus", + "auth_required_error": "Autoriseerimise vajalikkuse tuvastamine nurjus", + "cannot_connect": "\u00dchendamine nurjus", + "no_id": "Hyperion Ambilighti eksemplar ei teatanud oma ID-d" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end" + }, + "step": { + "auth": { + "data": { + "create_token": "Loo automaatselt uus juurdep\u00e4\u00e4sut\u00f5end", + "token": "V\u00f5i kasuta juba olemasolevat juurdep\u00e4\u00e4sut\u00f5endit" + }, + "description": "Seadista oma Hyperion Ambilighti serveri tuvastamine" + }, + "confirm": { + "description": "Kas soovid selle Hyperion Ambilighti lisada Home Assistanti?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Kinnita Hyperion Ambilight teenuse lisamine" + }, + "create_token": { + "description": "Uue juurdep\u00e4\u00e4sut\u00f5endi taotlemiseks vali allpool ** Esita **. Taotluse kinnitamiseks suunatakse Hyperioni kasutajaliidesesse. Palun kontrolli kas kuvatud ID on \" {auth_id} \"", + "title": "Loo automaatselt uus juurdep\u00e4\u00e4sut\u00f5end" + }, + "create_token_external": { + "title": "N\u00f5ustu uue juurdep\u00e4\u00e4sut\u00f5endiga Hyperion UI-s" + }, + "user": { + "data": { + "host": "", + "port": "" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "V\u00e4rvide ja efektide puhul on kasutatavad Hyperioni eelistused" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/lb.json b/homeassistant/components/ifttt/translations/lb.json index 56b3ba9ad81..64ef529ebc9 100644 --- a/homeassistant/components/ifttt/translations/lb.json +++ b/homeassistant/components/ifttt/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9ckemusst dir d'Aktioun \"Make a web request\" vum [IFTTT Webhook applet] ({applet_url}) benotzen.\n\nGitt folgend Informatiounen un:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nKuckt iech [Dokumentatioun]({docs_url}) w\u00e9i een Automatisatioune mat empfaangene Donn\u00e9e konfigur\u00e9iert." diff --git a/homeassistant/components/ipma/translations/en.json b/homeassistant/components/ipma/translations/en.json index 823bf9ab93b..67b38267a1c 100644 --- a/homeassistant/components/ipma/translations/en.json +++ b/homeassistant/components/ipma/translations/en.json @@ -15,5 +15,10 @@ "title": "Location" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API endpoint reachable" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/et.json b/homeassistant/components/ipma/translations/et.json index a45685c96e2..b5b1464c7e7 100644 --- a/homeassistant/components/ipma/translations/et.json +++ b/homeassistant/components/ipma/translations/et.json @@ -15,5 +15,10 @@ "title": "Asukoht" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API l\u00f5pp-punkt on k\u00e4ttesaadav" + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/lb.json b/homeassistant/components/kodi/translations/lb.json index c00f0e127bb..55dd0fac319 100644 --- a/homeassistant/components/kodi/translations/lb.json +++ b/homeassistant/components/kodi/translations/lb.json @@ -4,6 +4,7 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert", "cannot_connect": "Feeler beim verbannen", "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "no_uuid": "Kodi Instanz huet keng eenzegarteg ID. D\u00ebst ass wahrscheinlech duerch eng al Kodi versioun (17.x oder dr\u00ebnner). Du kanns d'Integratioun manuell konfigur\u00e9ieren oder op m\u00e9i eng rezent Kodi Versioun aktualis\u00e9ieren.", "unknown": "Onerwaarte Feeler" }, "error": { diff --git a/homeassistant/components/locative/translations/lb.json b/homeassistant/components/locative/translations/lb.json index bafc7d93459..58b87783d44 100644 --- a/homeassistant/components/locative/translations/lb.json +++ b/homeassistant/components/locative/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Plazen un Home Assistant ze sch\u00e9cken, muss den Webhook Feature an der Locative App ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." diff --git a/homeassistant/components/lovelace/translations/lb.json b/homeassistant/components/lovelace/translations/lb.json new file mode 100644 index 00000000000..deda8621adc --- /dev/null +++ b/homeassistant/components/lovelace/translations/lb.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Tableau de Bord", + "mode": "Modus", + "resources": "Ressourcen", + "views": "Usiichten" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/lb.json b/homeassistant/components/mailgun/translations/lb.json index 43564297cd5..6147d20fe69 100644 --- a/homeassistant/components/mailgun/translations/lb.json +++ b/homeassistant/components/mailgun/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren." diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json new file mode 100644 index 00000000000..b5c9c77397f --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "connection_error": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "host": "IP Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index b30e878368a..739d77c8268 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -39,10 +39,10 @@ }, "device_automation": { "trigger_type": { - "camera_person": "Person detected", "camera_motion": "Motion detected", + "camera_person": "Person detected", "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 7db77ad954e..da5d717cb37 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -36,5 +36,13 @@ "title": "Elija el m\u00e9todo de autenticaci\u00f3n" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Movimiento detectado", + "camera_person": "Persona detectada", + "camera_sound": "Sonido detectado", + "doorbell_chime": "Timbre pulsado" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 2e131b6f9fe..2e58ddeeddf 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -36,5 +36,13 @@ "title": "Vali tuvastusmeetod" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Tuvastati liikumine", + "camera_person": "Isik tuvastatud", + "camera_sound": "Tuvastati heli", + "doorbell_chime": "Uksekell helises" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json index 4c28b3becc6..1f0115a429b 100644 --- a/homeassistant/components/nest/translations/lb.json +++ b/homeassistant/components/nest/translations/lb.json @@ -31,5 +31,13 @@ "title": "Authentifikatioun's Method auswielen" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Bewegung erkannt", + "camera_person": "Persoun detekt\u00e9iert", + "camera_sound": "Toun detekt\u00e9iert", + "doorbell_chime": "Schell gedr\u00e9ckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index d18304c9f50..4060808c268 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -36,5 +36,13 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "camera_person": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430", + "camera_sound": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0437\u0432\u0443\u043a", + "doorbell_chime": "\u041d\u0430\u0436\u0430\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0437\u0432\u043e\u043d\u043a\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 0ceab12cfc5..80d0f8ee66a 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -36,5 +36,13 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\u5075\u6e2c\u5230\u52d5\u4f5c", + "camera_person": "\u5075\u6e2c\u5230\u4eba\u54e1", + "camera_sound": "\u5075\u6e2c\u5230\u8072\u97f3", + "doorbell_chime": "\u9580\u9234\u6309\u4e0b" + } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json index 180e2a51542..a298aa33671 100644 --- a/homeassistant/components/ozw/translations/et.json +++ b/homeassistant/components/ozw/translations/et.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "OpenZWave'i lisandmooduli k\u00e4ivitamine nurjus. Kontrolli s\u00e4tteid." }, + "progress": { + "install_addon": "Palun oota kuni OpenZWave lisandmooduli paigaldus l\u00f5peb. See v\u00f5ib v\u00f5tta mitu minutit." + }, "step": { + "install_addon": { + "title": "OpenZWave lisandmooduli paigaldamine on alanud" + }, "on_supervisor": { "data": { "use_addon": "Kasuta OpenZWave Supervisori lisandmoodulit" diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index f400a2c11ad..eee2386a921 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -18,5 +18,10 @@ "title": "Bekreft integrering p\u00e5 nytt" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API-endepunkt n\u00e5s" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index b97e27ced5d..13394e5a293 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -83,7 +83,7 @@ "remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\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_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } From cf5be049b375cf00df9c9c77529fd79d63223a81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 08:01:27 +0100 Subject: [PATCH 314/430] Warn when referencing missing devices/areas (#43787) --- homeassistant/helpers/service.py | 180 +++++++++++++++++++++---------- tests/helpers/test_service.py | 37 +++++++ 2 files changed, 162 insertions(+), 55 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 47918f31514..25a88bb59cb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,5 +1,6 @@ """Service calling related helpers.""" import asyncio +import dataclasses from functools import partial, wraps import logging from typing import ( @@ -37,8 +38,13 @@ from homeassistant.exceptions import ( Unauthorized, UnknownUser, ) -from homeassistant.helpers import device_registry, entity_registry, template -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import ( + area_registry, + config_validation as cv, + device_registry, + entity_registry, + template, +) from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType from homeassistant.loader import ( MAX_LOAD_CONCURRENTLY, @@ -64,6 +70,38 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" +@dataclasses.dataclass +class SelectedEntities: + """Class to hold the selected entities.""" + + # Entities that were explicitly mentioned. + referenced: Set[str] = dataclasses.field(default_factory=set) + + # Entities that were referenced via device/area ID. + # Should not trigger a warning when they don't exist. + indirectly_referenced: Set[str] = dataclasses.field(default_factory=set) + + # Referenced items that could not be found. + missing_devices: Set[str] = dataclasses.field(default_factory=set) + missing_areas: Set[str] = dataclasses.field(default_factory=set) + + def log_missing(self, missing_entities: Set[str]) -> None: + """Log about missing items.""" + parts = [] + for label, items in ( + ("areas", self.missing_areas), + ("devices", self.missing_devices), + ("entities", missing_entities), + ): + if items: + parts.append(f"{label} {', '.join(sorted(items))}") + + if not parts: + return + + _LOGGER.warning("Unable to find referenced %s", ", ".join(parts)) + + @bind_hass def call_from_config( hass: HomeAssistantType, @@ -186,25 +224,25 @@ async def async_extract_entities( if data_ent_id == ENTITY_MATCH_ALL: return [entity for entity in entities if entity.available] - entity_ids = await async_extract_entity_ids(hass, service_call, expand_group) + referenced = await async_extract_referenced_entity_ids( + hass, service_call, expand_group + ) + combined = referenced.referenced | referenced.indirectly_referenced found = [] for entity in entities: - if entity.entity_id not in entity_ids: + if entity.entity_id not in combined: continue - entity_ids.remove(entity.entity_id) + combined.remove(entity.entity_id) if not entity.available: continue found.append(entity) - if entity_ids: - _LOGGER.warning( - "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) - ) + referenced.log_missing(referenced.referenced & combined) return found @@ -213,10 +251,21 @@ async def async_extract_entities( async def async_extract_entity_ids( hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True ) -> Set[str]: - """Extract a list of entity ids from a service call. + """Extract a set of entity ids from a service call. Will convert group entity ids to the entity ids it represents. """ + referenced = await async_extract_referenced_entity_ids( + hass, service_call, expand_group + ) + return referenced.referenced | referenced.indirectly_referenced + + +@bind_hass +async def async_extract_referenced_entity_ids( + hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True +) -> SelectedEntities: + """Extract referenced entity IDs from a service call.""" entity_ids = service_call.data.get(ATTR_ENTITY_ID) device_ids = service_call.data.get(ATTR_DEVICE_ID) area_ids = service_call.data.get(ATTR_AREA_ID) @@ -225,12 +274,14 @@ async def async_extract_entity_ids( selects_device_ids = device_ids not in (None, ENTITY_MATCH_NONE) selects_area_ids = area_ids not in (None, ENTITY_MATCH_NONE) - extracted: Set[str] = set() + selected = SelectedEntities() if not selects_entity_ids and not selects_device_ids and not selects_area_ids: - return extracted + return selected if selects_entity_ids: + assert entity_ids is not None + # Entity ID attr can be a list or a string if isinstance(entity_ids, str): entity_ids = [entity_ids] @@ -238,58 +289,68 @@ async def async_extract_entity_ids( if expand_group: entity_ids = hass.components.group.expand_entity_ids(entity_ids) - extracted.update(entity_ids) + selected.referenced.update(entity_ids) if not selects_device_ids and not selects_area_ids: - return extracted + return selected - dev_reg, ent_reg = cast( - Tuple[device_registry.DeviceRegistry, entity_registry.EntityRegistry], + area_reg, dev_reg, ent_reg = cast( + Tuple[ + area_registry.AreaRegistry, + device_registry.DeviceRegistry, + entity_registry.EntityRegistry, + ], await asyncio.gather( + area_registry.async_get_registry(hass), device_registry.async_get_registry(hass), entity_registry.async_get_registry(hass), ), ) - if not selects_device_ids: - picked_devices = set() - elif isinstance(device_ids, str): - picked_devices = {device_ids} - else: - assert isinstance(device_ids, list) - picked_devices = set(device_ids) + picked_devices = set() + + if selects_device_ids: + if isinstance(device_ids, str): + picked_devices = {device_ids} + else: + assert isinstance(device_ids, list) + picked_devices = set(device_ids) + + for device_id in picked_devices: + if device_id not in dev_reg.devices: + selected.missing_devices.add(device_id) if selects_area_ids: - if isinstance(area_ids, str): - area_ids = [area_ids] + assert area_ids is not None - assert isinstance(area_ids, list) + if isinstance(area_ids, str): + area_lookup = {area_ids} + else: + area_lookup = set(area_ids) + + for area_id in area_lookup: + if area_id not in area_reg.areas: + selected.missing_areas.add(area_id) + continue # Find entities tied to an area - extracted.update( - entry.entity_id - for area_id in area_ids - for entry in entity_registry.async_entries_for_area(ent_reg, area_id) - ) + for entity_entry in ent_reg.entities.values(): + if entity_entry.area_id in area_lookup: + selected.indirectly_referenced.add(entity_entry.entity_id) - picked_devices.update( - [ - device.id - for area_id in area_ids - for device in device_registry.async_entries_for_area(dev_reg, area_id) - ] - ) + # Find devices for this area + for device_entry in dev_reg.devices.values(): + if device_entry.area_id in area_lookup: + picked_devices.add(device_entry.id) if not picked_devices: - return extracted + return selected - extracted.update( - entity_entry.entity_id - for entity_entry in ent_reg.entities.values() - if not entity_entry.area_id and entity_entry.device_id in picked_devices - ) + for entity_entry in ent_reg.entities.values(): + if not entity_entry.area_id and entity_entry.device_id in picked_devices: + selected.indirectly_referenced.add(entity_entry.entity_id) - return extracted + return selected def _load_services_file(hass: HomeAssistantType, integration: Integration) -> JSON_TYPE: @@ -416,9 +477,13 @@ async def entity_service_call( target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL - if not target_all_entities: + if target_all_entities: + referenced: Optional[SelectedEntities] = None + all_referenced: Optional[Set[str]] = None + else: # A set of entities we're trying to target. - entity_ids = await async_extract_entity_ids(hass, call, True) + referenced = await async_extract_referenced_entity_ids(hass, call, True) + all_referenced = referenced.referenced | referenced.indirectly_referenced # If the service function is a string, we'll pass it the service call data if isinstance(func, str): @@ -441,11 +506,12 @@ async def entity_service_call( if target_all_entities: entity_candidates.extend(platform.entities.values()) else: + assert all_referenced is not None entity_candidates.extend( [ entity for entity in platform.entities.values() - if entity.entity_id in entity_ids + if entity.entity_id in all_referenced ] ) @@ -462,11 +528,13 @@ async def entity_service_call( ) else: + assert all_referenced is not None + for platform in platforms: platform_entities = [] for entity in platform.entities.values(): - if entity.entity_id not in entity_ids: + if entity.entity_id not in all_referenced: continue if not entity_perms(entity.entity_id, POLICY_CONTROL): @@ -481,13 +549,15 @@ async def entity_service_call( entity_candidates.extend(platform_entities) if not target_all_entities: - for entity in entity_candidates: - entity_ids.remove(entity.entity_id) + assert referenced is not None - if entity_ids: - _LOGGER.warning( - "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) - ) + # Only report on explicit referenced entities + missing = set(referenced.referenced) + + for entity in entity_candidates: + missing.discard(entity.entity_id) + + referenced.log_missing(missing) entities = [] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index f9b09b259ca..a75593ddd40 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -960,3 +960,40 @@ async def test_extract_from_service_area_id(hass, area_mock): "light.in_area", "light.no_area", ] + + +async def test_entity_service_call_warn_referenced(hass, caplog): + """Test we only warn for referenced entities in entity_service_call.""" + call = ha.ServiceCall( + "light", + "turn_on", + { + "area_id": "non-existent-area", + "entity_id": "non.existent", + "device_id": "non-existent-device", + }, + ) + await service.entity_service_call(hass, {}, "", call) + assert ( + "Unable to find referenced areas non-existent-area, devices non-existent-device, entities non.existent" + in caplog.text + ) + + +async def test_async_extract_entities_warn_referenced(hass, caplog): + """Test we only warn for referenced entities in async_extract_entities.""" + call = ha.ServiceCall( + "light", + "turn_on", + { + "area_id": "non-existent-area", + "entity_id": "non.existent", + "device_id": "non-existent-device", + }, + ) + extracted = await service.async_extract_entities(hass, {}, call) + assert len(extracted) == 0 + assert ( + "Unable to find referenced areas non-existent-area, devices non-existent-device, entities non.existent" + in caplog.text + ) From 39e7b30ab61ce560cf8ab4fe0c5ef331e5b283bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Tue, 1 Dec 2020 08:36:36 +0100 Subject: [PATCH 315/430] Add lock.open service to nello (#42141) --- homeassistant/components/nello/lock.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index dc761d61461..61241660847 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -5,7 +5,7 @@ import logging from pynello.private import Nello import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -85,3 +85,13 @@ class NelloLock(LockEntity): """Unlock the device.""" if not self._nello_lock.open_door(): _LOGGER.error("Failed to unlock") + + def open(self, **kwargs): + """Unlock the device.""" + if not self._nello_lock.open_door(): + _LOGGER.error("Failed to open") + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN From 935ec59c56675ee6289db90affea472912b183c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 09:28:30 +0100 Subject: [PATCH 316/430] Migrate foscam to use entity platform entity services (#43775) --- homeassistant/components/foscam/camera.py | 51 ++++++++++------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 1c4c6bb9c8c..bc28e160b25 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -13,14 +13,9 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_extract_entity_ids +from homeassistant.helpers import config_validation as cv, entity_platform -from .const import ( - DATA as FOSCAM_DATA, - DOMAIN as FOSCAM_DOMAIN, - ENTITIES as FOSCAM_ENTITIES, -) +from .const import DATA as FOSCAM_DATA, ENTITIES as FOSCAM_ENTITIES _LOGGER = logging.getLogger(__name__) @@ -90,28 +85,26 @@ SERVICE_PTZ_SCHEMA = vol.Schema( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" - - 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 + platform = entity_platform.current_platform.get() + assert platform is not None + platform.async_register_entity_service( + "ptz", + { + 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, + }, + "async_perform_ptz", ) camera = FoscamCamera( From 14620e1573563ee5d15db15e0644f53b2667c129 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 09:28:41 +0100 Subject: [PATCH 317/430] Use entity platform for Neato (#43772) --- homeassistant/components/neato/const.py | 2 - homeassistant/components/neato/vacuum.py | 51 +++++++----------------- 2 files changed, 14 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 144ea40b92a..53948e2b19d 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -11,8 +11,6 @@ NEATO_ROBOTS = "neato_robots" SCAN_INTERVAL_MINUTES = 1 -SERVICE_NEATO_CUSTOM_CLEANING = "custom_cleaning" - VALID_VENDORS = ["neato", "vorwerk"] MODE = {1: "Eco", 2: "Turbo"} diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 841b160ad30..677bed1565b 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -25,8 +25,7 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers import config_validation as cv, entity_platform from .const import ( ACTION, @@ -39,7 +38,6 @@ from .const import ( NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, - SERVICE_NEATO_CUSTOM_CLEANING, ) _LOGGER = logging.getLogger(__name__) @@ -73,16 +71,6 @@ ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" ATTR_ZONE = "zone" -SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_MODE, default=2): cv.positive_int, - vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, - vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, - vol.Optional(ATTR_ZONE): cv.string, - } -) - async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato vacuum with config entry.""" @@ -99,30 +87,19 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Adding vacuums %s", dev) async_add_entities(dev, True) - def neato_custom_cleaning_service(call): - """Zone cleaning service that allows user to change options.""" - for robot in service_to_entities(call): - if call.service == SERVICE_NEATO_CUSTOM_CLEANING: - mode = call.data.get(ATTR_MODE) - navigation = call.data.get(ATTR_NAVIGATION) - category = call.data.get(ATTR_CATEGORY) - zone = call.data.get(ATTR_ZONE) - try: - robot.neato_custom_cleaning(mode, navigation, category, zone) - except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + platform = entity_platform.current_platform.get() + assert platform is not None - def service_to_entities(call): - """Return the known devices that a service call mentions.""" - entity_ids = extract_entity_ids(hass, call) - entities = [entity for entity in dev if entity.entity_id in entity_ids] - return entities - - hass.services.async_register( - NEATO_DOMAIN, - SERVICE_NEATO_CUSTOM_CLEANING, - neato_custom_cleaning_service, - schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA, + platform.async_register_entity_service( + "custom_cleaning", + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_MODE, default=2): cv.positive_int, + vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, + vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, + vol.Optional(ATTR_ZONE): cv.string, + }, + "neato_custom_cleaning", ) @@ -407,7 +384,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs): + def neato_custom_cleaning(self, mode, navigation, category, zone=None): """Zone cleaning service call.""" boundary_id = None if zone is not None: From 885f325812fbc6838441538f83044f2efea4b971 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Dec 2020 10:50:55 +0100 Subject: [PATCH 318/430] Upgrade elgato to 1.0.0 (#43792) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index f1a92ec727f..1da98a41211 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,7 +3,7 @@ "name": "Elgato Key Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", - "requirements": ["elgato==0.2.0"], + "requirements": ["elgato==1.0.0"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index ce8a2dcc93d..36ff1c84b77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -532,7 +532,7 @@ ecoaliface==0.4.0 eebrightbox==0.0.4 # homeassistant.components.elgato -elgato==0.2.0 +elgato==1.0.0 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 721eb3acfb3..0fce5009774 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ dynalite_devices==0.1.46 eebrightbox==0.0.4 # homeassistant.components.elgato -elgato==0.2.0 +elgato==1.0.0 # homeassistant.components.elkm1 elkm1-lib==0.8.8 From aad16b8055d28522b8807ff3c8ee4f95e93f9ffc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 1 Dec 2020 04:22:57 -0700 Subject: [PATCH 319/430] Bump aiorecollect to 0.2.2 (#43796) --- homeassistant/components/recollect_waste/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 6e1580ddf5c..4e6b71d59b7 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ - "aiorecollect==0.2.1" + "aiorecollect==0.2.2" ], "codeowners": [ "@bachya" diff --git a/requirements_all.txt b/requirements_all.txt index 36ff1c84b77..6f7424c7e6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -215,7 +215,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.recollect_waste -aiorecollect==0.2.1 +aiorecollect==0.2.2 # homeassistant.components.shelly aioshelly==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fce5009774..be2fa0ec0f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.recollect_waste -aiorecollect==0.2.1 +aiorecollect==0.2.2 # homeassistant.components.shelly aioshelly==0.5.1 From 69710cb6647b65308697453545ac8e07f60558b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 12:45:56 +0100 Subject: [PATCH 320/430] Base area IDs on initial name (#43804) --- homeassistant/helpers/area_registry.py | 30 ++++++++++++++++---------- tests/helpers/test_area_registry.py | 12 +++++++++++ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index b8f7952cd5a..1a919996f86 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,13 +1,13 @@ """Provide a way to connect devices to one physical location.""" from asyncio import Event, gather from collections import OrderedDict -from typing import Dict, Iterable, List, MutableMapping, Optional, cast +from typing import Container, Dict, Iterable, List, MutableMapping, Optional, cast import attr from homeassistant.core import callback from homeassistant.loader import bind_hass -import homeassistant.util.uuid as uuid_util +from homeassistant.util import slugify from .typing import HomeAssistantType @@ -22,8 +22,17 @@ SAVE_DELAY = 10 class AreaEntry: """Area Registry Entry.""" - name: Optional[str] = attr.ib(default=None) - id: str = attr.ib(factory=uuid_util.random_uuid_hex) + name: str = attr.ib() + id: Optional[str] = attr.ib(default=None) + + def generate_id(self, existing_ids: Container) -> None: + """Initialize ID.""" + suggestion = suggestion_base = slugify(self.name) + tries = 1 + while suggestion in existing_ids: + tries += 1 + suggestion = f"{suggestion_base}_{tries}" + object.__setattr__(self, "id", suggestion) class AreaRegistry: @@ -51,16 +60,15 @@ class AreaRegistry: if self._async_is_registered(name): raise ValueError("Name is already in use") - area = AreaEntry() + area = AreaEntry(name=name) + area.generate_id(self.areas) + assert area.id is not None self.areas[area.id] = area - - created = self._async_update(area.id, name=name) - + self.async_schedule_save() self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": created.id} + EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id} ) - - return created + return area async def async_delete(self, area_id: str) -> None: """Delete area.""" diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index f6a75fe3c30..4b6ca7da3fe 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -43,6 +43,7 @@ async def test_create_area(hass, registry, update_events): """Make sure that we can create an area.""" area = registry.async_create("mock") + assert area.id == "mock" assert area.name == "mock" assert len(registry.areas) == 1 @@ -68,6 +69,17 @@ async def test_create_area_with_name_already_in_use(hass, registry, update_event assert len(update_events) == 1 +async def test_create_area_with_id_already_in_use(registry): + """Make sure that we can't create an area with a name already in use.""" + area1 = registry.async_create("mock") + + updated_area1 = registry.async_update(area1.id, "New Name") + assert updated_area1.id == area1.id + + area2 = registry.async_create("mock") + assert area2.id == "mock_2" + + async def test_delete_area(hass, registry, update_events): """Make sure that we can delete an area.""" area = registry.async_create("mock") From 8f3989e7d9ccfa89d4f8cd8ed47091b1daf1ea1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 1 Dec 2020 14:21:25 +0000 Subject: [PATCH 321/430] Remove unused stuff from ZHA registries (#43786) --- .../components/zha/core/channels/general.py | 5 ----- .../components/zha/core/channels/hvac.py | 1 - .../components/zha/core/channels/lighting.py | 1 - .../zha/core/channels/measurement.py | 1 - .../components/zha/core/channels/security.py | 1 - .../components/zha/core/registries.py | 20 ------------------- 6 files changed, 29 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 8747355a21a..dc06d01e596 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -147,7 +147,6 @@ class LevelControlClientChannel(ClientChannel): @registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id) -@registries.LIGHT_CLUSTERS.register(general.LevelControl.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) class LevelControlChannel(ZigbeeChannel): """Channel for the LevelControl Zigbee cluster.""" @@ -222,10 +221,7 @@ class OnOffClientChannel(ClientChannel): """OnOff client channel.""" -@registries.BINARY_SENSOR_CLUSTERS.register(general.OnOff.cluster_id) @registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id) -@registries.LIGHT_CLUSTERS.register(general.OnOff.cluster_id) -@registries.SWITCH_CLUSTERS.register(general.OnOff.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) class OnOffChannel(ZigbeeChannel): """Channel for the OnOff Zigbee cluster.""" @@ -370,7 +366,6 @@ class PollControl(ZigbeeChannel): await self.set_long_poll_interval(self.LONG_POLL) -@registries.DEVICE_TRACKER_CLUSTERS.register(general.PowerConfiguration.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) class PowerConfigurationChannel(ZigbeeChannel): """Channel for the zigbee power configuration cluster.""" diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 1eedf51cd00..ac832aacc61 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -92,7 +92,6 @@ class Pump(ZigbeeChannel): """Pump channel.""" -@registries.CLIMATE_CLUSTERS.register(hvac.Thermostat.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) class ThermostatChannel(ZigbeeChannel): """Thermostat channel.""" diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 2828193f8cf..16223582c33 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -19,7 +19,6 @@ class ColorClientChannel(ClientChannel): @registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id) -@registries.LIGHT_CLUSTERS.register(lighting.Color.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) class ColorChannel(ZigbeeChannel): """Color channel.""" diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 949b7cbc138..64db1aa82ac 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -36,7 +36,6 @@ class IlluminanceMeasurement(ZigbeeChannel): REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] -@registries.BINARY_SENSOR_CLUSTERS.register(measurement.OccupancySensing.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) class OccupancySensing(ZigbeeChannel): """Occupancy Sensing channel.""" diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 156ada1e8f1..e37987bc821 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -109,7 +109,6 @@ class IasWd(ZigbeeChannel): ) -@registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasZone.cluster_id) class IASZoneChannel(ZigbeeChannel): """Channel for the IASZone Zigbee cluster.""" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 81521748da0..e2b4056cfaa 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -25,7 +25,6 @@ from .typing import ChannelType GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] PHILLIPS_REMOTE_CLUSTER = 0xFC00 - SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 @@ -80,15 +79,8 @@ SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR } -SWITCH_CLUSTERS = SetRegistry() - -BINARY_SENSOR_CLUSTERS = SetRegistry() -BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - BINDABLE_CLUSTERS = SetRegistry() CHANNEL_ONLY_CLUSTERS = SetRegistry() -CLIMATE_CLUSTERS = SetRegistry() -CUSTOM_CLUSTER_MAPPINGS = {} DEVICE_CLASS = { zigpy.profiles.zha.PROFILE_ID: { @@ -119,19 +111,7 @@ DEVICE_CLASS = { } DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) -DEVICE_TRACKER_CLUSTERS = SetRegistry() -LIGHT_CLUSTERS = SetRegistry() -OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() CLIENT_CHANNELS_REGISTRY = DictRegistry() - -COMPONENT_CLUSTERS = { - BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, - CLIMATE: CLIMATE_CLUSTERS, - DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, - LIGHT: LIGHT_CLUSTERS, - SWITCH: SWITCH_CLUSTERS, -} - ZIGBEE_CHANNEL_REGISTRY = DictRegistry() From 24ba5bcbafafbba07ec1f5f41107f9fb7c1c2a04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Dec 2020 17:06:49 +0100 Subject: [PATCH 322/430] Remove Ubee Router integration (ADR-0004) (#43809) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/ubee/__init__.py | 1 - .../components/ubee/device_tracker.py | 79 ------------------- homeassistant/components/ubee/manifest.json | 7 -- requirements_all.txt | 3 - 6 files changed, 92 deletions(-) delete mode 100644 homeassistant/components/ubee/__init__.py delete mode 100644 homeassistant/components/ubee/device_tracker.py delete mode 100644 homeassistant/components/ubee/manifest.json diff --git a/.coveragerc b/.coveragerc index f74bf42a7e3..522ce8172d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -959,7 +959,6 @@ omit = homeassistant/components/twilio_call/notify.py homeassistant/components/twilio_sms/notify.py homeassistant/components/twitter/notify.py - homeassistant/components/ubee/device_tracker.py homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/unifiled/* diff --git a/CODEOWNERS b/CODEOWNERS index 23eacec8f22..4b687e071ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -475,7 +475,6 @@ homeassistant/components/tts/* @pvizeli homeassistant/components/tuya/* @ollo69 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb -homeassistant/components/ubee/* @mzdrale homeassistant/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk homeassistant/components/upb/* @gwww diff --git a/homeassistant/components/ubee/__init__.py b/homeassistant/components/ubee/__init__.py deleted file mode 100644 index cc7b131a2bd..00000000000 --- a/homeassistant/components/ubee/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ubee component.""" diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py deleted file mode 100644 index 266acc49c09..00000000000 --- a/homeassistant/components/ubee/device_tracker.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Support for Ubee router.""" - -import logging - -from pyubee import Ubee -import voluptuous as vol - -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__) - -CONF_MODEL = "model" -DEFAULT_MODEL = "detect" - -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_MODEL, default=DEFAULT_MODEL): vol.Any( - "EVW32C-0N", - "EVW320B", - "EVW321B", - "EVW3200-Wifi", - "EVW3226@UPC", - "DVW32CB", - "DDW36C", - ), - } -) - - -def get_scanner(hass, config): - """Validate the configuration and return a Ubee scanner.""" - info = config[DOMAIN] - host = info[CONF_HOST] - username = info[CONF_USERNAME] - password = info[CONF_PASSWORD] - model = info[CONF_MODEL] - - ubee = Ubee(host, username, password, model) - if not ubee.login(): - _LOGGER.error("Login failed") - return None - - scanner = UbeeDeviceScanner(ubee) - return scanner - - -class UbeeDeviceScanner(DeviceScanner): - """This class queries a wireless Ubee router.""" - - def __init__(self, ubee): - """Initialize the Ubee scanner.""" - self._ubee = ubee - self._mac2name = {} - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - devices = self._get_connected_devices() - self._mac2name = devices - return list(devices) - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - return self._mac2name.get(device) - - def _get_connected_devices(self): - """List connected devices with pyubee.""" - if not self._ubee.session_active(): - self._ubee.login() - - return self._ubee.get_connected_devices() diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json deleted file mode 100644 index 0603ffe8757..00000000000 --- a/homeassistant/components/ubee/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "ubee", - "name": "Ubee Router", - "documentation": "https://www.home-assistant.io/integrations/ubee", - "requirements": ["pyubee==0.10"], - "codeowners": ["@mzdrale"] -} diff --git a/requirements_all.txt b/requirements_all.txt index 6f7424c7e6e..0f5362ff7dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1863,9 +1863,6 @@ pytradfri[async]==7.0.4 # homeassistant.components.trafikverket_weatherstation pytrafikverket==0.1.6.2 -# homeassistant.components.ubee -pyubee==0.10 - # homeassistant.components.uptimerobot pyuptimerobot==0.0.5 From b1c11eaaf88d0b5c80a1221e325808d224a2abff Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Dec 2020 17:07:28 +0100 Subject: [PATCH 323/430] Fix config validation tests for upcoming beta (#43811) --- tests/helpers/test_config_validation.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 7693efbb926..480b2280afe 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -710,13 +710,13 @@ def test_deprecated_with_invalidation_version(caplog, schema, version): is detected """ deprecated_schema = vol.All( - cv.deprecated("mars", invalidation_version="1.0.0"), schema + cv.deprecated("mars", invalidation_version="9999.99.9"), schema ) message = ( "The 'mars' option is deprecated, " "please remove it from your configuration. " - "This option will become invalid in version 1.0.0" + "This option will become invalid in version 9999.99.9" ) test_data = {"mars": True} @@ -763,14 +763,16 @@ def test_deprecated_with_replacement_key_and_invalidation_version( is detected """ deprecated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="1.0.0"), + cv.deprecated( + "mars", replacement_key="jupiter", invalidation_version="9999.99.9" + ), schema, ) warning = ( "The 'mars' option is deprecated, " "please replace it with 'jupiter'. This option will become " - "invalid in version 1.0.0" + "invalid in version 9999.99.9" ) test_data = {"mars": True} @@ -912,7 +914,7 @@ def test_deprecated_with_replacement_key_invalidation_version_default( cv.deprecated( "mars", replacement_key="jupiter", - invalidation_version="1.0.0", + invalidation_version="9999.99.9", default=False, ), schema, @@ -924,7 +926,7 @@ def test_deprecated_with_replacement_key_invalidation_version_default( assert ( "The 'mars' option is deprecated, " "please replace it with 'jupiter'. This option will become " - "invalid in version 1.0.0" + "invalid in version 9999.99.9" ) in caplog.text assert {"jupiter": True} == output From 00d0c3f98b40165390c01c992b11aef3534bc698 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Dec 2020 17:13:15 +0100 Subject: [PATCH 324/430] Remove invalidation version from Airvisual (#43810) --- homeassistant/components/airvisual/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 17a96629d60..956b168a665 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -49,7 +49,7 @@ DATA_LISTENER = "listener" DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) @callback From 52217f1f60b2dc6043c5c822a6767df21b596700 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 1 Dec 2020 17:28:59 +0100 Subject: [PATCH 325/430] Move uptime from relative time to absolute time (#43623) --- homeassistant/components/uptime/sensor.py | 63 +++++++------------ tests/components/uptime/test_sensor.py | 76 +---------------------- 2 files changed, 22 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 12c00c7f96d..8363d2da2cb 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -1,47 +1,42 @@ """Platform to retrieve uptime for Home Assistant.""" -import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT 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 = "Uptime" -ICON = "mdi:clock" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="days"): vol.All( - cv.string, vol.In(["minutes", "hours", "days", "seconds"]) - ), - } +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_UNIT_OF_MEASUREMENT), + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="days"): vol.All( + cv.string, vol.In(["minutes", "hours", "days", "seconds"]) + ), + } + ), ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the uptime sensor platform.""" name = config.get(CONF_NAME) - units = config.get(CONF_UNIT_OF_MEASUREMENT) - async_add_entities([UptimeSensor(name, units)], True) + async_add_entities([UptimeSensor(name)], True) class UptimeSensor(Entity): """Representation of an uptime sensor.""" - def __init__(self, name, unit): + def __init__(self, name): """Initialize the uptime sensor.""" self._name = name - self._unit = unit - self.initial = dt_util.now() - self._state = None + self._state = dt_util.now().isoformat() @property def name(self): @@ -49,32 +44,16 @@ class UptimeSensor(Entity): return self._name @property - def icon(self): - """Icon to display in the front end.""" - return ICON - - @property - def unit_of_measurement(self): - """Return the unit of measurement the value is expressed in.""" - return self._unit + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_TIMESTAMP @property def state(self): """Return the state of the sensor.""" return self._state - async def async_update(self): - """Update the state of the sensor.""" - delta = dt_util.now() - self.initial - div_factor = 3600 - - if self.unit_of_measurement == "days": - div_factor *= 24 - elif self.unit_of_measurement == "minutes": - div_factor /= 60 - elif self.unit_of_measurement == "seconds": - div_factor /= 3600 - - delta = delta.total_seconds() / div_factor - self._state = round(delta, 2) - _LOGGER.debug("New value: %s", delta) + @property + def should_poll(self) -> bool: + """Disable polling for this entity.""" + return False diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index fd0c03239f7..fe3ae30a843 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,85 +1,11 @@ """The tests for the uptime sensor platform.""" -from datetime import timedelta -from homeassistant.components.uptime.sensor import UptimeSensor from homeassistant.setup import async_setup_component -from tests.async_mock import patch - - -async def test_uptime_min_config(hass): - """Test minimum uptime configuration.""" - config = {"sensor": {"platform": "uptime"}} - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - state = hass.states.get("sensor.uptime") - assert state.attributes.get("unit_of_measurement") == "days" - async def test_uptime_sensor_name_change(hass): """Test uptime sensor with different name.""" config = {"sensor": {"platform": "uptime", "name": "foobar"}} assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - state = hass.states.get("sensor.foobar") - assert state.attributes.get("unit_of_measurement") == "days" - - -async def test_uptime_sensor_config_hours(hass): - """Test uptime sensor with hours defined in config.""" - config = {"sensor": {"platform": "uptime", "unit_of_measurement": "hours"}} - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - state = hass.states.get("sensor.uptime") - assert state.attributes.get("unit_of_measurement") == "hours" - - -async def test_uptime_sensor_config_minutes(hass): - """Test uptime sensor with minutes defined in config.""" - config = {"sensor": {"platform": "uptime", "unit_of_measurement": "minutes"}} - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - state = hass.states.get("sensor.uptime") - assert state.attributes.get("unit_of_measurement") == "minutes" - - -async def test_uptime_sensor_days_output(hass): - """Test uptime sensor output data.""" - sensor = UptimeSensor("test", "days") - assert sensor.unit_of_measurement == "days" - new_time = sensor.initial + timedelta(days=1) - with patch("homeassistant.util.dt.now", return_value=new_time): - await sensor.async_update() - assert sensor.state == 1.00 - new_time = sensor.initial + timedelta(days=111.499) - with patch("homeassistant.util.dt.now", return_value=new_time): - await sensor.async_update() - assert sensor.state == 111.50 - - -async def test_uptime_sensor_hours_output(hass): - """Test uptime sensor output data.""" - sensor = UptimeSensor("test", "hours") - assert sensor.unit_of_measurement == "hours" - new_time = sensor.initial + timedelta(hours=16) - with patch("homeassistant.util.dt.now", return_value=new_time): - await sensor.async_update() - assert sensor.state == 16.00 - new_time = sensor.initial + timedelta(hours=72.499) - with patch("homeassistant.util.dt.now", return_value=new_time): - await sensor.async_update() - assert sensor.state == 72.50 - - -async def test_uptime_sensor_minutes_output(hass): - """Test uptime sensor output data.""" - sensor = UptimeSensor("test", "minutes") - assert sensor.unit_of_measurement == "minutes" - new_time = sensor.initial + timedelta(minutes=16) - with patch("homeassistant.util.dt.now", return_value=new_time): - await sensor.async_update() - assert sensor.state == 16.00 - new_time = sensor.initial + timedelta(minutes=12.499) - with patch("homeassistant.util.dt.now", return_value=new_time): - await sensor.async_update() - assert sensor.state == 12.50 + assert hass.states.get("sensor.foobar") From 7d23ff65118aa585a964250f440182a2a65ac14c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 18:14:01 +0100 Subject: [PATCH 326/430] Add device action to mobile app to notify (#43814) --- .coveragerc | 1 - .../components/mobile_app/__init__.py | 1 + homeassistant/components/mobile_app/const.py | 1 + .../components/mobile_app/device_action.py | 87 +++++++++++++++++++ homeassistant/components/mobile_app/notify.py | 19 ++-- .../components/mobile_app/strings.json | 5 ++ .../mobile_app/translations/en.json | 5 ++ homeassistant/components/mobile_app/util.py | 47 ++++++++++ homeassistant/components/notify/__init__.py | 22 ++--- .../integration/device_action.py | 4 +- .../device_action/tests/test_device_action.py | 2 +- .../integration/device_condition.py | 2 +- .../tests/test_device_condition.py | 2 +- .../integration/device_trigger.py | 7 +- .../tests/test_device_trigger.py | 2 +- tests/components/mobile_app/conftest.py | 20 +++++ .../mobile_app/test_device_action.py | 68 +++++++++++++++ 17 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/mobile_app/device_action.py create mode 100644 homeassistant/components/mobile_app/util.py create mode 100644 tests/components/mobile_app/test_device_action.py diff --git a/.coveragerc b/.coveragerc index 522ce8172d1..40ae410eadc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -541,7 +541,6 @@ omit = homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py - homeassistant/components/mobile_app/* homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 264017796aa..3bc95bf3e05 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -123,6 +123,7 @@ async def async_unload_entry(hass, entry): webhook_unregister(hass, webhook_id) del hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + del hass.data[DOMAIN][DATA_DEVICES][webhook_id] await hass_notify.async_reload(hass, DOMAIN) return True diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 6174e34f57a..b35468a6fb3 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -15,6 +15,7 @@ DATA_DELETED_IDS = "deleted_ids" DATA_DEVICES = "devices" DATA_SENSOR = "sensor" DATA_STORE = "store" +DATA_NOTIFY = "notify" ATTR_APP_DATA = "app_data" ATTR_APP_ID = "app_id" diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py new file mode 100644 index 00000000000..2592d4b486b --- /dev/null +++ b/homeassistant/components/mobile_app/device_action.py @@ -0,0 +1,87 @@ +"""Provides device actions for Mobile App.""" +from typing import List, Optional + +import voluptuous as vol + +from homeassistant.components import notify +from homeassistant.components.device_automation import InvalidDeviceAutomationConfig +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, template + +from .const import DOMAIN +from .util import get_notify_service, supports_push, webhook_id_from_device_id + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): "notify", + vol.Required(notify.ATTR_MESSAGE): cv.template, + vol.Optional(notify.ATTR_TITLE): cv.template, + vol.Optional(notify.ATTR_DATA): cv.template_complex, + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Mobile App devices.""" + webhook_id = webhook_id_from_device_id(hass, device_id) + + if webhook_id is None or not supports_push(hass, webhook_id): + return [] + + return [{CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_TYPE: "notify"}] + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + webhook_id = webhook_id_from_device_id(hass, config[CONF_DEVICE_ID]) + + if webhook_id is None: + raise InvalidDeviceAutomationConfig( + "Unable to resolve webhook ID from the device ID" + ) + + service_name = get_notify_service(hass, webhook_id) + + if service_name is None: + raise InvalidDeviceAutomationConfig( + "Unable to find notify service for webhook ID" + ) + + service_data = {notify.ATTR_TARGET: webhook_id} + + # Render it here because we have access to variables here. + for key in (notify.ATTR_MESSAGE, notify.ATTR_TITLE, notify.ATTR_DATA): + if key not in config: + continue + + value_template = config[key] + template.attach(hass, value_template) + + try: + service_data[key] = template.render_complex(value_template, variables) + except template.TemplateError as err: + raise InvalidDeviceAutomationConfig( + f"Error rendering {key}: {err}" + ) from err + + await hass.services.async_call( + notify.DOMAIN, service_name, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + if config[CONF_TYPE] != "notify": + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Required(notify.ATTR_MESSAGE): str, + vol.Optional(notify.ATTR_TITLE): str, + } + ) + } diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 04d308a5a05..b3482a70fb9 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -35,8 +35,10 @@ from .const import ( ATTR_PUSH_TOKEN, ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, + DATA_NOTIFY, DOMAIN, ) +from .util import supports_push _LOGGER = logging.getLogger(__name__) @@ -44,15 +46,13 @@ _LOGGER = logging.getLogger(__name__) def push_registrations(hass): """Return a dictionary of push enabled registrations.""" targets = {} + for webhook_id, entry in hass.data[DOMAIN][DATA_CONFIG_ENTRIES].items(): - data = entry.data - app_data = data[ATTR_APP_DATA] - if ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data: - device_name = data[ATTR_DEVICE_NAME] - if device_name in targets: - _LOGGER.warning("Found duplicate device name %s", device_name) - continue - targets[device_name] = webhook_id + if not supports_push(hass, webhook_id): + continue + + targets[entry.data[ATTR_DEVICE_NAME]] = webhook_id + return targets @@ -84,7 +84,8 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): async def async_get_service(hass, config, discovery_info=None): """Get the mobile_app notification service.""" session = async_get_clientsession(hass) - return MobileAppNotificationService(session) + service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session) + return service class MobileAppNotificationService(BaseNotificationService): diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json index fd1cddbcb5f..b18f3e7265c 100644 --- a/homeassistant/components/mobile_app/strings.json +++ b/homeassistant/components/mobile_app/strings.json @@ -8,5 +8,10 @@ "abort": { "install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps." } + }, + "device_automation": { + "action_type": { + "notify": "Send a notification" + } } } diff --git a/homeassistant/components/mobile_app/translations/en.json b/homeassistant/components/mobile_app/translations/en.json index 6def5e98582..34631f86afa 100644 --- a/homeassistant/components/mobile_app/translations/en.json +++ b/homeassistant/components/mobile_app/translations/en.json @@ -8,5 +8,10 @@ "description": "Do you want to set up the Mobile App component?" } } + }, + "device_automation": { + "action_type": { + "notify": "Send a notification" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py new file mode 100644 index 00000000000..d9a5f1643c7 --- /dev/null +++ b/homeassistant/components/mobile_app/util.py @@ -0,0 +1,47 @@ +"""Mobile app utility functions.""" +from typing import TYPE_CHECKING, Optional + +from homeassistant.core import callback + +from .const import ( + ATTR_APP_DATA, + ATTR_PUSH_TOKEN, + ATTR_PUSH_URL, + DATA_CONFIG_ENTRIES, + DATA_DEVICES, + DATA_NOTIFY, + DOMAIN, +) + +if TYPE_CHECKING: + from .notify import MobileAppNotificationService + + +@callback +def webhook_id_from_device_id(hass, device_id: str) -> Optional[str]: + """Get webhook ID from device ID.""" + for cur_webhook_id, cur_device in hass.data[DOMAIN][DATA_DEVICES].items(): + if cur_device.id == device_id: + return cur_webhook_id + + return None + + +@callback +def supports_push(hass, webhook_id: str) -> bool: + """Return if push notifications is supported.""" + config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + app_data = config_entry.data[ATTR_APP_DATA] + return ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data + + +@callback +def get_notify_service(hass, webhook_id: str) -> Optional[str]: + """Return the notify service for this webhook ID.""" + notify_service: "MobileAppNotificationService" = hass.data[DOMAIN][DATA_NOTIFY] + + for target_service, target_webhook_id in notify_service.registered_targets.items(): + if target_webhook_id == webhook_id: + return target_service + + return None diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 08219567ed6..426c28bccff 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -110,6 +110,8 @@ class BaseNotificationService: """An abstract class for notification services.""" hass: Optional[HomeAssistantType] = None + # Name => target + registered_targets: Dict[str, str] def send_message(self, message, **kwargs): """Send a message. @@ -135,8 +137,8 @@ class BaseNotificationService: title.hass = self.hass kwargs[ATTR_TITLE] = title.async_render(parse_result=False) - if self._registered_targets.get(service.service) is not None: - kwargs[ATTR_TARGET] = [self._registered_targets[service.service]] + if self.registered_targets.get(service.service) is not None: + kwargs[ATTR_TARGET] = [self.registered_targets[service.service]] elif service.data.get(ATTR_TARGET) is not None: kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) @@ -157,23 +159,23 @@ class BaseNotificationService: self.hass = hass self._service_name = service_name self._target_service_name_prefix = target_service_name_prefix - self._registered_targets: Dict = {} + self.registered_targets = {} async def async_register_services(self) -> None: """Create or update the notify services.""" assert self.hass if hasattr(self, "targets"): - stale_targets = set(self._registered_targets) + stale_targets = set(self.registered_targets) # pylint: disable=no-member for name, target in self.targets.items(): # type: ignore target_name = slugify(f"{self._target_service_name_prefix}_{name}") if target_name in stale_targets: stale_targets.remove(target_name) - if target_name in self._registered_targets: + if target_name in self.registered_targets: continue - self._registered_targets[target_name] = target + self.registered_targets[target_name] = target self.hass.services.async_register( DOMAIN, target_name, @@ -182,7 +184,7 @@ class BaseNotificationService: ) for stale_target_name in stale_targets: - del self._registered_targets[stale_target_name] + del self.registered_targets[stale_target_name] self.hass.services.async_remove( DOMAIN, stale_target_name, @@ -202,10 +204,10 @@ class BaseNotificationService: """Unregister the notify services.""" assert self.hass - if self._registered_targets: - remove_targets = set(self._registered_targets) + if self.registered_targets: + remove_targets = set(self.registered_targets) for remove_target_name in remove_targets: - del self._registered_targets[remove_target_name] + del self.registered_targets[remove_target_name] self.hass.services.async_remove( DOMAIN, remove_target_name, diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index 3861ee8ebe9..27a27cb95ee 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -1,4 +1,4 @@ -"""Provides device automations for NEW_NAME.""" +"""Provides device actions for NEW_NAME.""" from typing import List, Optional import voluptuous as vol @@ -72,8 +72,6 @@ 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": 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 3c7c7bb71a4..91a4693ebeb 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -1,8 +1,8 @@ """The tests for NEW_NAME device actions.""" import pytest +from homeassistant.components import automation from homeassistant.components.NEW_DOMAIN import DOMAIN -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index cb2489e4279..6ad89332f8e 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 @@ -"""Provide the device automations for NEW_NAME.""" +"""Provide the device conditions for NEW_NAME.""" from typing import Dict, List import voluptuous as vol 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 34217a61f9e..07e0afd05eb 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -1,8 +1,8 @@ """The tests for NEW_NAME device conditions.""" import pytest +from homeassistant.components import automation from homeassistant.components.NEW_DOMAIN import DOMAIN -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 diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index e1312148cbe..7709813957e 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -1,4 +1,4 @@ -"""Provides device automations for NEW_NAME.""" +"""Provides device triggers for NEW_NAME.""" from typing import List import voluptuous as vol @@ -80,11 +80,8 @@ async def async_attach_trigger( 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. + # Use the existing state or event triggers from the automation integration. if config[CONF_TYPE] == "turned_on": from_state = STATE_OFF 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 82540566318..23daaf8dadd 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -1,8 +1,8 @@ """The tests for NEW_NAME device triggers.""" import pytest +from homeassistant.components import automation from homeassistant.components.NEW_DOMAIN import DOMAIN -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 diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index e15c5732ac4..7c611eb1010 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -40,6 +40,26 @@ async def create_registrations(hass, authed_api_client): return (enc_reg_json, clear_reg_json) +@pytest.fixture +async def push_registration(hass, authed_api_client): + """Return registration with push notifications enabled.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + enc_reg = await authed_api_client.post( + "/api/mobile_app/registrations", + json={ + **REGISTER, + "app_data": { + "push_url": "http://localhost/mock-push", + "push_token": "abcd", + }, + }, + ) + + assert enc_reg.status == 201 + return await enc_reg.json() + + @pytest.fixture async def webhook_client(hass, authed_api_client, aiohttp_client): """mobile_app mock client.""" diff --git a/tests/components/mobile_app/test_device_action.py b/tests/components/mobile_app/test_device_action.py new file mode 100644 index 00000000000..e5b15412e4d --- /dev/null +++ b/tests/components/mobile_app/test_device_action.py @@ -0,0 +1,68 @@ +"""The tests for Mobile App device actions.""" +from homeassistant.components import automation, device_automation +from homeassistant.components.mobile_app import DATA_DEVICES, DOMAIN, util +from homeassistant.setup import async_setup_component + +from tests.common import async_get_device_automations, patch + + +async def test_get_actions(hass, push_registration): + """Test we get the expected actions from a mobile_app.""" + webhook_id = push_registration["webhook_id"] + device_id = hass.data[DOMAIN][DATA_DEVICES][webhook_id].id + + assert await async_get_device_automations(hass, "action", device_id) == [ + {"domain": DOMAIN, "device_id": device_id, "type": "notify"} + ] + + capabilitites = await device_automation._async_get_device_automation_capabilities( + hass, "action", {"domain": DOMAIN, "device_id": device_id, "type": "notify"} + ) + assert "extra_fields" in capabilitites + + +async def test_action(hass, push_registration): + """Test for turn_on and turn_off actions.""" + webhook_id = push_registration["webhook_id"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_notify", + }, + "action": [ + {"variables": {"name": "Paulus"}}, + { + "domain": DOMAIN, + "device_id": hass.data[DOMAIN]["devices"][webhook_id].id, + "type": "notify", + "message": "Hello {{ name }}", + }, + ], + }, + ] + }, + ) + + service_name = util.get_notify_service(hass, webhook_id) + + # Make sure it was actually registered + assert hass.services.has_service("notify", service_name) + + with patch( + "homeassistant.components.mobile_app.notify.MobileAppNotificationService.async_send_message" + ) as mock_send_message: + hass.bus.async_fire("test_notify") + await hass.async_block_till_done() + assert len(mock_send_message.mock_calls) == 1 + + assert mock_send_message.mock_calls[0][2] == { + "target": [webhook_id], + "message": "Hello Paulus", + "data": None, + } From 1c9c99571e6558162099b203a8cb29f1753aefc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 18:21:36 +0100 Subject: [PATCH 327/430] Use !input instead of !placeholder (#43820) * Use !input instead of !placeholder * Update input name * Lint * Move tests around --- .devcontainer/devcontainer.json | 2 +- .../automation/blueprints/motion_light.yaml | 10 ++-- .../blueprints/notify_leaving_zone.yaml | 6 +- .../components/blueprint/__init__.py | 2 +- homeassistant/components/blueprint/errors.py | 8 +-- homeassistant/components/blueprint/models.py | 19 ++----- homeassistant/util/yaml/__init__.py | 8 ++- homeassistant/util/yaml/dumper.py | 6 +- .../placeholder.py => util/yaml/input.py} | 29 +++++----- homeassistant/util/yaml/loader.py | 4 +- homeassistant/util/yaml/objects.py | 6 +- tests/components/blueprint/test_importer.py | 29 ++++++---- tests/components/blueprint/test_models.py | 56 +++++++++---------- .../blueprint/test_websocket_api.py | 2 +- tests/fixtures/blueprint/community_post.json | 2 +- tests/fixtures/blueprint/github_gist.json | 2 +- tests/helpers/test_check_config.py | 4 +- tests/helpers/test_placeholder.py | 29 ---------- .../in_folder/in_folder_blueprint.yaml | 4 +- .../automation/test_event_service.yaml | 4 +- tests/util/yaml/__init__.py | 1 + .../util/{test_yaml.py => yaml/test_init.py} | 20 +++---- tests/util/yaml/test_input.py | 34 +++++++++++ 23 files changed, 148 insertions(+), 139 deletions(-) rename homeassistant/{helpers/placeholder.py => util/yaml/input.py} (57%) delete mode 100644 tests/helpers/test_placeholder.py create mode 100644 tests/util/yaml/__init__.py rename tests/util/{test_yaml.py => yaml/test_init.py} (97%) create mode 100644 tests/util/yaml/test_input.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3dc26ed2a94..e01a97425e1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,7 @@ "files.trimTrailingWhitespace": true, "terminal.integrated.shell.linux": "/bin/bash", "yaml.customTags": [ - "!placeholder scalar", + "!input scalar", "!secret scalar", "!include_dir_named scalar", "!include_dir_list scalar", diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index 6cf368f9c0d..cd6e5c7b281 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -31,18 +31,18 @@ max_exceeded: silent trigger: platform: state - entity_id: !placeholder motion_entity + entity_id: !input motion_entity from: "off" to: "on" action: - service: homeassistant.turn_on - target: !placeholder light_target + target: !input light_target - wait_for_trigger: platform: state - entity_id: !placeholder motion_entity + entity_id: !input motion_entity from: "on" to: "off" - - delay: !placeholder no_motion_wait + - delay: !input no_motion_wait - service: homeassistant.turn_off - target: !placeholder light_target + target: !input light_target diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 7e7dba8bea8..178c3222c0a 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -18,10 +18,10 @@ blueprint: trigger: platform: state - entity_id: !placeholder person_entity + entity_id: !input person_entity variables: - zone_entity: !placeholder zone_entity + zone_entity: !input zone_entity zone_state: "{{ states[zone_entity].name }}" condition: @@ -29,6 +29,6 @@ condition: value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" action: - - service: !placeholder notify_service + - service: !input notify_service data: message: "{{ trigger.to_state.name }} has left {{ zone_state }}" diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 12a6782065b..9e8b1260eff 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -7,7 +7,7 @@ from .errors import ( # noqa FailedToLoad, InvalidBlueprint, InvalidBlueprintInputs, - MissingPlaceholder, + MissingInput, ) from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa from .schemas import is_blueprint_instance_config # noqa diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 4a12fde1c26..b422b2dcbe3 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -66,17 +66,17 @@ class InvalidBlueprintInputs(BlueprintException): ) -class MissingPlaceholder(BlueprintWithNameException): - """When we miss a placeholder.""" +class MissingInput(BlueprintWithNameException): + """When we miss an input.""" def __init__( - self, domain: str, blueprint_name: str, placeholder_names: Iterable[str] + self, domain: str, blueprint_name: str, input_names: Iterable[str] ) -> None: """Initialize blueprint exception.""" super().__init__( domain, blueprint_name, - f"Missing placeholder {', '.join(sorted(placeholder_names))}", + f"Missing input {', '.join(sorted(input_names))}", ) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 73417722dcc..32fc30b60b9 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -19,7 +19,6 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import placeholder from homeassistant.util import yaml from .const import ( @@ -38,7 +37,7 @@ from .errors import ( FileAlreadyExists, InvalidBlueprint, InvalidBlueprintInputs, - MissingPlaceholder, + MissingInput, ) from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA @@ -59,8 +58,6 @@ class Blueprint: except vol.Invalid as err: raise InvalidBlueprint(expected_domain, path, data, err) from err - self.placeholders = placeholder.extract_placeholders(data) - # In future, we will treat this as "incorrect" and allow to recover from this data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN] if expected_domain is not None and data_domain != expected_domain: @@ -73,7 +70,7 @@ class Blueprint: self.domain = data_domain - missing = self.placeholders - set(data[CONF_BLUEPRINT][CONF_INPUT]) + missing = yaml.extract_inputs(data) - set(data[CONF_BLUEPRINT][CONF_INPUT]) if missing: raise InvalidBlueprint( @@ -143,7 +140,7 @@ class BlueprintInputs: @property def inputs_with_default(self): """Return the inputs and fallback to defaults.""" - no_input = self.blueprint.placeholders - set(self.inputs) + no_input = set(self.blueprint.inputs) - set(self.inputs) inputs_with_default = dict(self.inputs) @@ -156,12 +153,10 @@ class BlueprintInputs: def validate(self) -> None: """Validate the inputs.""" - missing = self.blueprint.placeholders - set(self.inputs_with_default) + missing = set(self.blueprint.inputs) - set(self.inputs_with_default) if missing: - raise MissingPlaceholder( - self.blueprint.domain, self.blueprint.name, missing - ) + raise MissingInput(self.blueprint.domain, self.blueprint.name, missing) # In future we can see if entities are correct domain, areas exist etc # using the new selector helper. @@ -169,9 +164,7 @@ class BlueprintInputs: @callback def async_substitute(self) -> dict: """Get the blueprint value with the inputs substituted.""" - processed = placeholder.substitute( - self.blueprint.data, self.inputs_with_default - ) + processed = yaml.substitute(self.blueprint.data, self.inputs_with_default) combined = {**processed, **self.config_with_inputs} # From config_with_inputs combined.pop(CONF_USE_BLUEPRINT) diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py index bb6eb122de5..ac4ac2f9a16 100644 --- a/homeassistant/util/yaml/__init__.py +++ b/homeassistant/util/yaml/__init__.py @@ -1,17 +1,21 @@ """YAML utility functions.""" from .const import _SECRET_NAMESPACE, SECRET_YAML from .dumper import dump, save_yaml +from .input import UndefinedSubstitution, extract_inputs, substitute from .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml -from .objects import Placeholder +from .objects import Input __all__ = [ "SECRET_YAML", "_SECRET_NAMESPACE", - "Placeholder", + "Input", "dump", "save_yaml", "clear_secret_cache", "load_yaml", "secret_yaml", "parse_yaml", + "UndefinedSubstitution", + "extract_inputs", + "substitute", ] diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 6834323ed72..8e9cb382b6c 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -3,7 +3,7 @@ from collections import OrderedDict import yaml -from .objects import NodeListClass, Placeholder +from .objects import Input, NodeListClass # mypy: allow-untyped-calls, no-warn-return-any @@ -62,6 +62,6 @@ yaml.SafeDumper.add_representer( ) yaml.SafeDumper.add_representer( - Placeholder, - lambda dumper, value: dumper.represent_scalar("!placeholder", value.name), + Input, + lambda dumper, value: dumper.represent_scalar("!input", value.name), ) diff --git a/homeassistant/helpers/placeholder.py b/homeassistant/util/yaml/input.py similarity index 57% rename from homeassistant/helpers/placeholder.py rename to homeassistant/util/yaml/input.py index 3da5eaba76f..6282509fae2 100644 --- a/homeassistant/helpers/placeholder.py +++ b/homeassistant/util/yaml/input.py @@ -1,45 +1,46 @@ -"""Placeholder helpers.""" +"""Deal with YAML input.""" + from typing import Any, Dict, Set -from homeassistant.util.yaml import Placeholder +from .objects import Input class UndefinedSubstitution(Exception): """Error raised when we find a substitution that is not defined.""" - def __init__(self, placeholder: str) -> None: + def __init__(self, input_name: str) -> None: """Initialize the undefined substitution exception.""" - super().__init__(f"No substitution found for placeholder {placeholder}") - self.placeholder = placeholder + super().__init__(f"No substitution found for input {input_name}") + self.input = input -def extract_placeholders(obj: Any) -> Set[str]: - """Extract placeholders from a structure.""" +def extract_inputs(obj: Any) -> Set[str]: + """Extract input from a structure.""" found: Set[str] = set() - _extract_placeholders(obj, found) + _extract_inputs(obj, found) return found -def _extract_placeholders(obj: Any, found: Set[str]) -> None: - """Extract placeholders from a structure.""" - if isinstance(obj, Placeholder): +def _extract_inputs(obj: Any, found: Set[str]) -> None: + """Extract input from a structure.""" + if isinstance(obj, Input): found.add(obj.name) return if isinstance(obj, list): for val in obj: - _extract_placeholders(val, found) + _extract_inputs(val, found) return if isinstance(obj, dict): for val in obj.values(): - _extract_placeholders(val, found) + _extract_inputs(val, found) return def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any: """Substitute values.""" - if isinstance(obj, Placeholder): + if isinstance(obj, Input): if obj.name not in substitutions: raise UndefinedSubstitution(obj.name) return substitutions[obj.name] diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index c9e191db5de..18beb2d4b14 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -11,7 +11,7 @@ import yaml from homeassistant.exceptions import HomeAssistantError from .const import _SECRET_NAMESPACE, SECRET_YAML -from .objects import NodeListClass, NodeStrClass, Placeholder +from .objects import Input, NodeListClass, NodeStrClass try: import keyring @@ -331,4 +331,4 @@ yaml.SafeLoader.add_constructor("!include_dir_named", _include_dir_named_yaml) yaml.SafeLoader.add_constructor( "!include_dir_merge_named", _include_dir_merge_named_yaml ) -yaml.SafeLoader.add_constructor("!placeholder", Placeholder.from_node) +yaml.SafeLoader.add_constructor("!input", Input.from_node) diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index c3f4c0ff140..0e46820e0db 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -13,12 +13,12 @@ class NodeStrClass(str): @dataclass(frozen=True) -class Placeholder: - """A placeholder that should be substituted.""" +class Input: + """Input that should be substituted.""" name: str @classmethod - def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Placeholder": + def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Input": """Create a new placeholder from a node.""" return cls(node.value) diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 9d6bb48bcaf..bb8903459c9 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -57,9 +57,9 @@ def test_extract_blueprint_from_community_topic(community_post): ) assert imported_blueprint is not None assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "service_to_call", - "trigger_event", + assert imported_blueprint.blueprint.inputs == { + "service_to_call": None, + "trigger_event": None, } @@ -103,9 +103,9 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit ) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "service_to_call", - "trigger_event", + assert imported_blueprint.blueprint.inputs == { + "service_to_call": None, + "trigger_event": None, } assert imported_blueprint.suggested_filename == "balloob/test-topic" assert ( @@ -133,9 +133,9 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "service_to_call", - "trigger_event", + assert imported_blueprint.blueprint.inputs == { + "service_to_call": None, + "trigger_event": None, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url @@ -152,9 +152,14 @@ async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock): imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "motion_entity", - "light_entity", + assert imported_blueprint.blueprint.inputs == { + "motion_entity": { + "name": "Motion Sensor", + "selector": { + "entity": {"domain": "binary_sensor", "device_class": "motion"} + }, + }, + "light_entity": {"name": "Light", "selector": {"entity": {"domain": "light"}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 3680889e56b..6e15bd952a4 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -4,7 +4,7 @@ import logging import pytest from homeassistant.components.blueprint import errors, models -from homeassistant.util.yaml import Placeholder +from homeassistant.util.yaml import Input from tests.async_mock import patch @@ -18,18 +18,16 @@ def blueprint_1(): "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", - "input": { - "test-placeholder": {"name": "Name", "description": "Description"} - }, + "input": {"test-input": {"name": "Name", "description": "Description"}}, }, - "example": Placeholder("test-placeholder"), + "example": Input("test-input"), } ) @pytest.fixture def blueprint_2(): - """Blueprint fixture with default placeholder.""" + """Blueprint fixture with default inputs.""" return models.Blueprint( { "blueprint": { @@ -37,12 +35,12 @@ def blueprint_2(): "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "input": { - "test-placeholder": {"name": "Name", "description": "Description"}, - "test-placeholder-default": {"default": "test"}, + "test-input": {"name": "Name", "description": "Description"}, + "test-input-default": {"default": "test"}, }, }, - "example": Placeholder("test-placeholder"), - "example-default": Placeholder("test-placeholder-default"), + "example": Input("test-input"), + "example-default": Input("test-input-default"), } ) @@ -72,7 +70,7 @@ def test_blueprint_model_init(): "domain": "automation", "input": {"something": None}, }, - "trigger": {"platform": Placeholder("non-existing")}, + "trigger": {"platform": Input("non-existing")}, } ) @@ -83,11 +81,13 @@ def test_blueprint_properties(blueprint_1): "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", - "input": {"test-placeholder": {"name": "Name", "description": "Description"}}, + "input": {"test-input": {"name": "Name", "description": "Description"}}, } assert blueprint_1.domain == "automation" assert blueprint_1.name == "Hello" - assert blueprint_1.placeholders == {"test-placeholder"} + assert blueprint_1.inputs == { + "test-input": {"name": "Name", "description": "Description"} + } def test_blueprint_update_metadata(): @@ -140,13 +140,13 @@ def test_blueprint_inputs(blueprint_2): { "use_blueprint": { "path": "bla", - "input": {"test-placeholder": 1, "test-placeholder-default": 12}, + "input": {"test-input": 1, "test-input-default": 12}, }, "example-default": {"overridden": "via-config"}, }, ) inputs.validate() - assert inputs.inputs == {"test-placeholder": 1, "test-placeholder-default": 12} + assert inputs.inputs == {"test-input": 1, "test-input-default": 12} assert inputs.async_substitute() == { "example": 1, "example-default": {"overridden": "via-config"}, @@ -159,7 +159,7 @@ def test_blueprint_inputs_validation(blueprint_1): blueprint_1, {"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}}, ) - with pytest.raises(errors.MissingPlaceholder): + with pytest.raises(errors.MissingInput): inputs.validate() @@ -167,13 +167,13 @@ def test_blueprint_inputs_default(blueprint_2): """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, - {"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}}, + {"use_blueprint": {"path": "bla", "input": {"test-input": 1}}}, ) inputs.validate() - assert inputs.inputs == {"test-placeholder": 1} + assert inputs.inputs == {"test-input": 1} assert inputs.inputs_with_default == { - "test-placeholder": 1, - "test-placeholder-default": "test", + "test-input": 1, + "test-input-default": "test", } assert inputs.async_substitute() == {"example": 1, "example-default": "test"} @@ -185,18 +185,18 @@ def test_blueprint_inputs_override_default(blueprint_2): { "use_blueprint": { "path": "bla", - "input": {"test-placeholder": 1, "test-placeholder-default": "custom"}, + "input": {"test-input": 1, "test-input-default": "custom"}, } }, ) inputs.validate() assert inputs.inputs == { - "test-placeholder": 1, - "test-placeholder-default": "custom", + "test-input": 1, + "test-input-default": "custom", } assert inputs.inputs_with_default == { - "test-placeholder": 1, - "test-placeholder-default": "custom", + "test-input": 1, + "test-input-default": "custom", } assert inputs.async_substitute() == {"example": 1, "example-default": "custom"} @@ -238,7 +238,7 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1): with pytest.raises(errors.InvalidBlueprintInputs): await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"}) - with pytest.raises(errors.MissingPlaceholder), patch.object( + with pytest.raises(errors.MissingInput), patch.object( domain_bps, "async_get_blueprint", return_value=blueprint_1 ): await domain_bps.async_inputs_from_config( @@ -247,10 +247,10 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1): with patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1): inputs = await domain_bps.async_inputs_from_config( - {"use_blueprint": {"path": "bla.yaml", "input": {"test-placeholder": None}}} + {"use_blueprint": {"path": "bla.yaml", "input": {"test-input": None}}} ) assert inputs.blueprint is blueprint_1 - assert inputs.inputs == {"test-placeholder": None} + assert inputs.inputs == {"test-input": None} async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index d79463ac3a6..c4f39127d93 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !placeholder 'trigger_event'\naction:\n service: !placeholder 'service_to_call'\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n", ) diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json index 28684ec65f7..5b9a3dcb9c7 100644 --- a/tests/fixtures/blueprint/community_post.json +++ b/tests/fixtures/blueprint/community_post.json @@ -7,7 +7,7 @@ "username": "balloob", "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", "created_at": "2020-10-16T12:20:12.688Z", - "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !placeholder trigger_event\naction:\n service: !placeholder service_to_call\n\u003c/code\u003e\u003c/pre\u003e", + "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e", "post_number": 1, "post_type": 1, "updated_at": "2020-10-20T08:24:14.189Z", diff --git a/tests/fixtures/blueprint/github_gist.json b/tests/fixtures/blueprint/github_gist.json index f25c7ca0238..208e8b54a71 100644 --- a/tests/fixtures/blueprint/github_gist.json +++ b/tests/fixtures/blueprint/github_gist.json @@ -15,7 +15,7 @@ "raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml", "size": 803, "truncated": false, - "content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !placeholder light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !placeholder light_entity\n" + "content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !input motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !input light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !input motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !input light_entity\n" } }, "public": false, diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 13ce52a840f..7959cf66403 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -159,9 +159,9 @@ blueprint: service_to_call: trigger: platform: event - event_type: !placeholder trigger_event + event_type: !input trigger_event action: - service: !placeholder service_to_call + service: !input service_to_call """, } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): diff --git a/tests/helpers/test_placeholder.py b/tests/helpers/test_placeholder.py deleted file mode 100644 index d5978cd465a..00000000000 --- a/tests/helpers/test_placeholder.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test placeholders.""" -import pytest - -from homeassistant.helpers import placeholder -from homeassistant.util.yaml import Placeholder - - -def test_extract_placeholders(): - """Test extracting placeholders from data.""" - assert placeholder.extract_placeholders(Placeholder("hello")) == {"hello"} - assert placeholder.extract_placeholders( - {"info": [1, Placeholder("hello"), 2, Placeholder("world")]} - ) == {"hello", "world"} - - -def test_substitute(): - """Test we can substitute.""" - assert placeholder.substitute(Placeholder("hello"), {"hello": 5}) == 5 - - with pytest.raises(placeholder.UndefinedSubstitution): - placeholder.substitute(Placeholder("hello"), {}) - - assert ( - placeholder.substitute( - {"info": [1, Placeholder("hello"), 2, Placeholder("world")]}, - {"hello": 5, "world": 10}, - ) - == {"info": [1, 5, 2, 10]} - ) diff --git a/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml b/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml index c869e30c41e..baaaf3df1ea 100644 --- a/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml +++ b/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml @@ -4,5 +4,5 @@ blueprint: input: trigger: action: -trigger: !placeholder trigger -action: !placeholder action +trigger: !input trigger +action: !input action diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index 0e9479cd8c3..eff8b52db16 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -6,6 +6,6 @@ blueprint: service_to_call: trigger: platform: event - event_type: !placeholder trigger_event + event_type: !input trigger_event action: - service: !placeholder service_to_call + service: !input service_to_call diff --git a/tests/util/yaml/__init__.py b/tests/util/yaml/__init__.py new file mode 100644 index 00000000000..5b5c1b8f15a --- /dev/null +++ b/tests/util/yaml/__init__.py @@ -0,0 +1 @@ +"""Tests for YAML util.""" diff --git a/tests/util/test_yaml.py b/tests/util/yaml/test_init.py similarity index 97% rename from tests/util/test_yaml.py rename to tests/util/yaml/test_init.py index 2e9d1b471ac..1c5b9bd9fd8 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/yaml/test_init.py @@ -463,18 +463,18 @@ def test_duplicate_key(caplog): assert "contains duplicate key" in caplog.text -def test_placeholder_class(): - """Test placeholder class.""" - placeholder = yaml_loader.Placeholder("hello") - placeholder2 = yaml_loader.Placeholder("hello") +def test_input_class(): + """Test input class.""" + input = yaml_loader.Input("hello") + input2 = yaml_loader.Input("hello") - assert placeholder.name == "hello" - assert placeholder == placeholder2 + assert input.name == "hello" + assert input == input2 - assert len({placeholder, placeholder2}) == 1 + assert len({input, input2}) == 1 -def test_placeholder(): - """Test loading placeholders.""" - data = {"hello": yaml.Placeholder("test_name")} +def test_input(): + """Test loading inputs.""" + data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data diff --git a/tests/util/yaml/test_input.py b/tests/util/yaml/test_input.py new file mode 100644 index 00000000000..1c13d1b3684 --- /dev/null +++ b/tests/util/yaml/test_input.py @@ -0,0 +1,34 @@ +"""Test inputs.""" +import pytest + +from homeassistant.util.yaml import ( + Input, + UndefinedSubstitution, + extract_inputs, + substitute, +) + + +def test_extract_inputs(): + """Test extracting inputs from data.""" + assert extract_inputs(Input("hello")) == {"hello"} + assert extract_inputs({"info": [1, Input("hello"), 2, Input("world")]}) == { + "hello", + "world", + } + + +def test_substitute(): + """Test we can substitute.""" + assert substitute(Input("hello"), {"hello": 5}) == 5 + + with pytest.raises(UndefinedSubstitution): + substitute(Input("hello"), {}) + + assert ( + substitute( + {"info": [1, Input("hello"), 2, Input("world")]}, + {"hello": 5, "world": 10}, + ) + == {"info": [1, 5, 2, 10]} + ) From 9ace7269ae047850b8bdd6541769dc9b57f5599f Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 1 Dec 2020 18:45:08 +0100 Subject: [PATCH 328/430] Fix wrong temperature setting in LCN climate (#43818) --- homeassistant/components/lcn/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index f55734a052f..8b0f4951bf9 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -142,7 +142,7 @@ class LcnClimate(LcnDevice, ClimateEntity): return if not await self.address_connection.var_abs( - self.setpoint, self._target_temperature, self.unit + self.setpoint, temperature, self.unit ): return self._target_temperature = temperature From 1faef0a4d429e8c636beed2728ad564f379f650c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 1 Dec 2020 18:49:58 +0100 Subject: [PATCH 329/430] Make simple deCONZ thermostats work (#43781) --- homeassistant/components/deconz/climate.py | 9 +- tests/components/deconz/test_climate.py | 121 +++++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index afa9e8dcd9f..0c1fe2da1e3 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -72,7 +72,12 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): super().__init__(device, gateway) self._hvac_modes = dict(HVAC_MODES) - if "coolsetpoint" not in device.raw["config"]: + if "mode" not in device.raw["config"]: + self._hvac_modes = { + HVAC_MODE_HEAT: True, + HVAC_MODE_OFF: False, + } + elif "coolsetpoint" not in device.raw["config"]: self._hvac_modes.pop(HVAC_MODE_COOL) self._features = SUPPORT_TARGET_TEMPERATURE @@ -110,6 +115,8 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): raise ValueError(f"Unsupported HVAC mode {hvac_mode}") data = {"mode": self._hvac_modes[hvac_mode]} + if len(self._hvac_modes) == 2: # Only allow turn on and off thermostat + data = {"on": self._hvac_modes[hvac_mode]} await self._device.async_set_config(data) diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 751f1572239..cea660b855b 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -73,6 +73,127 @@ async def test_no_sensors(hass): assert len(hass.states.async_all()) == 0 +async def test_simple_climate_device(hass): + """Test successful creation of climate entities. + + This is a simple water heater that only supports setting temperature and on and off. + """ + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": { + "battery": 59, + "displayflipped": None, + "heatsetpoint": 2100, + "locked": None, + "mountingmode": None, + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "6130553ac247174809bae47144ee23f8", + "lastseen": "2020-11-29T19:31Z", + "manufacturername": "Danfoss", + "modelid": "eTRV0100", + "name": "thermostat", + "state": { + "errorcode": None, + "lastupdated": "2020-11-29T19:28:40.665", + "mountingmodeactive": False, + "on": True, + "temperature": 2102, + "valve": 24, + "windowopen": "Closed", + }, + "swversion": "01.02.0008 01.02", + "type": "ZHAThermostat", + "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 2 + climate_thermostat = hass.states.get("climate.thermostat") + assert climate_thermostat.state == HVAC_MODE_HEAT + assert climate_thermostat.attributes["hvac_modes"] == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert climate_thermostat.attributes["current_temperature"] == 21.0 + assert climate_thermostat.attributes["temperature"] == 21.0 + assert hass.states.get("sensor.thermostat_battery_level").state == "59" + + # Event signals thermostat configured off + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "state": {"on": False}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.thermostat").state == STATE_OFF + + # Event signals thermostat state on + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.thermostat").state == HVAC_MODE_HEAT + + # Verify service calls + + thermostat_device = gateway.api.sensors["0"] + + # Service turn on thermostat + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/sensors/0/config", json={"on": True}) + + # Service turn on thermostat + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/sensors/0/config", json={"on": False}) + + # Service set HVAC mode to unsupported value + + with patch.object( + thermostat_device, "_request", return_value=True + ) as set_callback, pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + blocking=True, + ) + + async def test_climate_device_without_cooling_support(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) From 0ee86ea8e4a1b91b0c3b0604d7d9e824efcf8472 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Dec 2020 19:26:43 +0100 Subject: [PATCH 330/430] Another try to get rid of Shelly flaky test (#43821) --- tests/components/shelly/conftest.py | 11 +++++++++++ tests/components/shelly/test_config_flow.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/components/shelly/conftest.py diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py new file mode 100644 index 00000000000..3f97c0ef317 --- /dev/null +++ b/tests/components/shelly/conftest.py @@ -0,0 +1,11 @@ +"""Test configuration for Shelly.""" +import pytest + +from tests.async_mock import patch + + +@pytest.fixture(autouse=True) +def mock_coap(): + """Mock out coap.""" + with patch("homeassistant.components.shelly.get_coap_context"): + yield diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index bef018e7d91..1796847bd74 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch("aioshelly.COAP", return_value=Mock(initialize=AsyncMock())), patch( + with patch( "aioshelly.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ), patch( From 032d569cd1ef25675f4cbc912a17d81db4257e47 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 1 Dec 2020 14:34:07 -0500 Subject: [PATCH 331/430] Add Analog cluster for Lumi plugs (#43817) --- homeassistant/components/zha/sensor.py | 5 +++++ tests/components/zha/zha_devices_list.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index eff3892630b..b02b3a549be 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -145,6 +145,11 @@ class Sensor(ZhaEntity): return round(float(value * self._multiplier) / self._divisor) +@STRICT_MATCH( + channel_names=CHANNEL_ANALOG_INPUT, + manufacturers="LUMI", + models={"lumi.plug", "lumi.plug.maus01", "lumi.plug.mmeu01"}, +) @STRICT_MATCH(channel_names=CHANNEL_ANALOG_INPUT, manufacturers="Digi") class AnalogInput(Sensor): """Sensor that displays analog input values.""" diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index a6287592395..136af1f4be9 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -1372,10 +1372,22 @@ DEVICES = [ }, }, "entities": [ + "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", "switch.lumi_lumi_plug_maus01_77665544_on_off", ], "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-2-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + }, ("switch", "00:11:22:33:44:55:66:77-1"): { "channels": ["on_off"], "entity_class": "Switch", From fc42f59b0b0a544d0fdaf5dbafc99bc8fbddfbd6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 2 Dec 2020 00:03:14 +0000 Subject: [PATCH 332/430] [ci skip] Translation update --- .../components/bsblan/translations/cs.json | 4 +- .../components/bsblan/translations/es.json | 4 +- .../components/bsblan/translations/et.json | 4 +- .../components/bsblan/translations/no.json | 4 +- .../components/bsblan/translations/ru.json | 4 +- .../bsblan/translations/zh-Hant.json | 4 +- .../cover/translations/zh-Hans.json | 10 +++- .../components/hyperion/translations/cs.json | 21 ++++++++ .../components/hyperion/translations/es.json | 52 +++++++++++++++++++ .../components/hyperion/translations/no.json | 52 +++++++++++++++++++ .../components/hyperion/translations/ru.json | 52 +++++++++++++++++++ .../hyperion/translations/zh-Hant.json | 52 +++++++++++++++++++ .../components/ipma/translations/es.json | 5 ++ .../components/ipma/translations/no.json | 5 ++ .../components/ipma/translations/ru.json | 5 ++ .../components/ipma/translations/zh-Hant.json | 5 ++ .../mobile_app/translations/ru.json | 5 ++ .../components/nest/translations/no.json | 8 +++ .../components/ozw/translations/es.json | 6 +++ .../components/ozw/translations/no.json | 6 +++ .../components/ozw/translations/ru.json | 6 +++ .../components/ozw/translations/zh-Hant.json | 6 +++ .../sensor/translations/zh-Hans.json | 12 ++++- .../switch/translations/zh-Hans.json | 7 ++- .../components/tag/translations/zh-Hans.json | 3 ++ 25 files changed, 330 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/hyperion/translations/cs.json create mode 100644 homeassistant/components/hyperion/translations/es.json create mode 100644 homeassistant/components/hyperion/translations/no.json create mode 100644 homeassistant/components/hyperion/translations/ru.json create mode 100644 homeassistant/components/hyperion/translations/zh-Hant.json create mode 100644 homeassistant/components/tag/translations/zh-Hans.json diff --git a/homeassistant/components/bsblan/translations/cs.json b/homeassistant/components/bsblan/translations/cs.json index 3df55116d19..f0bce62991f 100644 --- a/homeassistant/components/bsblan/translations/cs.json +++ b/homeassistant/components/bsblan/translations/cs.json @@ -11,7 +11,9 @@ "user": { "data": { "host": "Hostitel", - "port": "Port" + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed BSB-Lan" } diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 287bd0fb49d..691136f5441 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -12,7 +12,9 @@ "data": { "host": "Host", "passkey": "Clave de acceso", - "port": "Puerto" + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" }, "description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.", "title": "Conectar con el dispositivo BSB-Lan" diff --git a/homeassistant/components/bsblan/translations/et.json b/homeassistant/components/bsblan/translations/et.json index 70f35535112..22ff91e7e1b 100644 --- a/homeassistant/components/bsblan/translations/et.json +++ b/homeassistant/components/bsblan/translations/et.json @@ -12,7 +12,9 @@ "data": { "host": "", "passkey": "Juurdep\u00e4\u00e4sut\u00f5endi string", - "port": "" + "password": "Salas\u00f5na", + "port": "", + "username": "Kasutajanimi" }, "description": "Seadista oma BSB-Lan seadme sidumine Home Assistant'iga.", "title": "\u00dchendu BSB-Lan seadmega" diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json index 685f3afb1f4..40981e2b77c 100644 --- a/homeassistant/components/bsblan/translations/no.json +++ b/homeassistant/components/bsblan/translations/no.json @@ -12,7 +12,9 @@ "data": { "host": "Vert", "passkey": "Tilgangsn\u00f8kkel streng", - "port": "Port" + "password": "Passord", + "port": "Port", + "username": "Brukernavn" }, "description": "Konfigurer din BSB-Lan-enhet for \u00e5 integrere med Home Assistant.", "title": "Koble til BSB-Lan-enheten" diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json index 7d7bcb9fa0e..76aa715a9de 100644 --- a/homeassistant/components/bsblan/translations/ru.json +++ b/homeassistant/components/bsblan/translations/ru.json @@ -12,7 +12,9 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "passkey": "\u041f\u0430\u0440\u043e\u043b\u044c", - "port": "\u041f\u043e\u0440\u0442" + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BSB-Lan.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json index 3d3bcb44ac7..7ada76c1d21 100644 --- a/homeassistant/components/bsblan/translations/zh-Hant.json +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -12,7 +12,9 @@ "data": { "host": "\u4e3b\u6a5f\u7aef", "passkey": "Passkey \u5b57\u4e32", - "port": "\u901a\u8a0a\u57e0" + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8a2d\u5b9a BSB-Lan \u8a2d\u5099\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", "title": "\u9023\u7dda\u81f3 BSB-Lan \u8a2d\u5099" diff --git a/homeassistant/components/cover/translations/zh-Hans.json b/homeassistant/components/cover/translations/zh-Hans.json index ccc1edd42c5..7c5675dad31 100644 --- a/homeassistant/components/cover/translations/zh-Hans.json +++ b/homeassistant/components/cover/translations/zh-Hans.json @@ -1,9 +1,15 @@ { "device_automation": { + "action_type": { + "stop": "\u505c\u6b62 {entity_name}" + }, "condition_type": { "is_closed": "{entity_name} \u5df2\u5173\u95ed", - "is_closing": "{entity_name}\u6b63\u5728\u5173\u95ed", - "is_open": "{entity_name}\u4e3a\u5f00\u653e" + "is_closing": "{entity_name} \u6b63\u5728\u5173\u95ed", + "is_open": "{entity_name} \u5df2\u6253\u5f00", + "is_opening": "{entity_name} \u6b63\u5728\u6253\u5f00", + "is_position": "{entity_name} \u5f53\u524d\u4f4d\u7f6e\u4e3a", + "is_tilt_position": "{entity_name} \u5f53\u524d\u503e\u659c\u4f4d\u7f6e\u4e3a" }, "trigger_type": { "closed": "{entity_name}\u5df2\u5173\u95ed" diff --git a/homeassistant/components/hyperion/translations/cs.json b/homeassistant/components/hyperion/translations/cs.json new file mode 100644 index 00000000000..c5358988bac --- /dev/null +++ b/homeassistant/components/hyperion/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json new file mode 100644 index 00000000000..bb1ef3e2c03 --- /dev/null +++ b/homeassistant/components/hyperion/translations/es.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la interfaz de usuario de Hyperion", + "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", + "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", + "cannot_connect": "No se pudo conectar", + "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_access_token": "Token de acceso no v\u00e1lido" + }, + "step": { + "auth": { + "data": { + "create_token": "Crea un nuevo token autom\u00e1ticamente", + "token": "O proporcionar un token preexistente" + }, + "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight" + }, + "confirm": { + "description": "\u00bfQuieres a\u00f1adir este Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**Identificaci\u00f3n**: {id}", + "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight" + }, + "create_token": { + "description": "Elige ** Enviar ** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Se te redirigir\u00e1 a la interfaz de usuario de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", + "title": "Crear autom\u00e1ticamente un nuevo token de autenticaci\u00f3n" + }, + "create_token_external": { + "title": "Aceptar nuevo token en la interfaz de usuario de Hyperion" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Prioridad de Hyperion a usar para colores y efectos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json new file mode 100644 index 00000000000..79c90379f18 --- /dev/null +++ b/homeassistant/components/hyperion/translations/no.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "auth_new_token_not_granted_error": "Nyopprettet token ble ikke godkjent p\u00e5 Hyperion UI", + "auth_new_token_not_work_error": "Kunne ikke godkjenne ved hjelp av nylig opprettet token", + "auth_required_error": "Kan ikke fastsl\u00e5 om autorisasjon er n\u00f8dvendig", + "cannot_connect": "Tilkobling mislyktes", + "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_access_token": "Ugyldig tilgangstoken" + }, + "step": { + "auth": { + "data": { + "create_token": "Opprett nytt token automatisk", + "token": "Eller oppgi eksisterende token" + }, + "description": "Konfigurer autorisasjon til Hyperion Ambilight-serveren" + }, + "confirm": { + "description": "Vil du legge til denne Hyperion Ambilight i Home Assistant? \n\n ** Vert: ** {host}\n ** Port: ** {port}\n ** ID **: {id}", + "title": "Bekreft tillegg av Hyperion Ambilight-tjenesten" + }, + "create_token": { + "description": "Velg **Send** nedenfor for \u00e5 be om et nytt godkjenningstoken. Du vil bli omdirigert til Hyperion UI for \u00e5 godkjenne foresp\u00f8rselen. Kontroller at den viste IDen er {auth_id}.", + "title": "Opprett nytt godkjenningstoken automatisk" + }, + "create_token_external": { + "title": "Godta nytt token i Hyperion UI" + }, + "user": { + "data": { + "host": "Vert", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion-prioritet for bruke til farger og effekter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ru.json b/homeassistant/components/hyperion/translations/ru.json new file mode 100644 index 00000000000..fda9ef4bb5b --- /dev/null +++ b/homeassistant/components/hyperion/translations/ru.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "auth_new_token_not_granted_error": "\u0421\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u043d\u0435 \u0431\u044b\u043b \u043e\u0434\u043e\u0431\u0440\u0435\u043d \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Hyperion.", + "auth_new_token_not_work_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.", + "auth_required_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043b\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "no_id": "Hyperion Ambilight \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0438\u043b \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + }, + "step": { + "auth": { + "data": { + "create_token": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d", + "token": "\u0418\u043b\u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0442\u043e\u043a\u0435\u043d" + }, + "description": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 Hyperion Ambilight." + }, + "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 Hyperion Ambilight?\n\n**\u0425\u043e\u0441\u0442:** {host}\n**\u041f\u043e\u0440\u0442:** {port}\n**ID**: {id}", + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b Hyperion Ambilight" + }, + "create_token": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c** \u043d\u0438\u0436\u0435, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Hyperion \u0434\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 - \"{auth_id}\"", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "create_token_external": { + "title": "\u041f\u0440\u0438\u043d\u044f\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Hyperion" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u0446\u0432\u0435\u0442\u043e\u0432 \u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u043e\u0432" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json new file mode 100644 index 00000000000..fb9cbe3b7a8 --- /dev/null +++ b/homeassistant/components/hyperion/translations/zh-Hant.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "auth_new_token_not_granted_error": "\u65b0\u5275\u5bc6\u9470\u672a\u7372\u5f97 Hyperion UI \u6838\u51c6", + "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u5bc6\u9470\u8a8d\u8b49\u5931\u6557", + "auth_required_error": "\u7121\u6cd5\u5224\u5b9a\u662f\u5426\u9700\u8981\u9a57\u8b49", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548" + }, + "step": { + "auth": { + "data": { + "create_token": "\u81ea\u52d5\u65b0\u5275\u5bc6\u9470", + "token": "\u6216\u63d0\u4f9b\u73fe\u6709\u5bc6\u9470" + }, + "description": "\u8a2d\u5b9a Hyperion Ambilight \u4f3a\u670d\u5668\u8a8d\u8b49" + }, + "confirm": { + "description": "\u662f\u5426\u8981\u5c07 Hyperion Ambilight \u65b0\u589e\u81f3 Home Assistant\uff1f\n\n**\u4e3b\u6a5f\u7aef\uff1a** {host}\n**\u901a\u8a0a\u57e0\uff1a** {port}\n**ID**\uff1a {id}", + "title": "\u78ba\u8a8d\u9644\u52a0 Hyperion Ambilight \u670d\u52d9" + }, + "create_token": { + "description": "\u9ede\u9078\u4e0b\u65b9 **\u50b3\u9001** \u4ee5\u8acb\u6c42\u65b0\u8a8d\u8b49\u5bc6\u9470\u3002\u5c07\u6703\u91cd\u65b0\u5c0e\u5411\u81f3 Hyperion UI \u4ee5\u6838\u51c6\u8981\u6c42\u3002\u8acb\u78ba\u8a8d\u986f\u793a ID \u70ba \"{auth_id}\"", + "title": "\u81ea\u52d5\u65b0\u5275\u8a8d\u8b49\u5bc6\u9470" + }, + "create_token_external": { + "title": "\u63a5\u53d7 Hyperion UI \u4e2d\u7684\u65b0\u5bc6\u9470" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion \u512a\u5148\u4f7f\u7528\u4e4b\u8272\u6eab\u8207\u7279\u6548" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/es.json b/homeassistant/components/ipma/translations/es.json index a3f83c150e7..d942608ad87 100644 --- a/homeassistant/components/ipma/translations/es.json +++ b/homeassistant/components/ipma/translations/es.json @@ -15,5 +15,10 @@ "title": "Ubicaci\u00f3n" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API IPMA" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/no.json b/homeassistant/components/ipma/translations/no.json index 4e4cdebeec6..1108b347110 100644 --- a/homeassistant/components/ipma/translations/no.json +++ b/homeassistant/components/ipma/translations/no.json @@ -15,5 +15,10 @@ "title": "Plassering" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API-endepunkt n\u00e5s" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/ru.json b/homeassistant/components/ipma/translations/ru.json index b9e98886e63..08e1673eef7 100644 --- a/homeassistant/components/ipma/translations/ru.json +++ b/homeassistant/components/ipma/translations/ru.json @@ -15,5 +15,10 @@ "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API IPMA" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/zh-Hant.json b/homeassistant/components/ipma/translations/zh-Hant.json index a3d084ddaa3..fc329a756b4 100644 --- a/homeassistant/components/ipma/translations/zh-Hant.json +++ b/homeassistant/components/ipma/translations/zh-Hant.json @@ -15,5 +15,10 @@ "title": "\u5ea7\u6a19" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API \u53ef\u9054\u7aef\u9ede" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/ru.json b/homeassistant/components/mobile_app/translations/ru.json index 7bb103b852e..fc4496ba1d8 100644 --- a/homeassistant/components/mobile_app/translations/ru.json +++ b/homeassistant/components/mobile_app/translations/ru.json @@ -8,5 +8,10 @@ "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 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435?" } } + }, + "device_automation": { + "action_type": { + "notify": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 624ecb357ee..69d67c5b4f2 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -36,5 +36,13 @@ "title": "Velg godkjenningsmetode" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Bevegelse oppdaget", + "camera_person": "Person oppdaget", + "camera_sound": "Lyd oppdaget", + "doorbell_chime": "Ringeklokke trykket" + } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json index 8d39e611601..60d64f9afbf 100644 --- a/homeassistant/components/ozw/translations/es.json +++ b/homeassistant/components/ozw/translations/es.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "No se pudo iniciar el complemento OpenZWave. Verifica la configuraci\u00f3n." }, + "progress": { + "install_addon": "Espera mientras finaliza la instalaci\u00f3n del complemento OpenZWave. Esto puede tardar varios minutos." + }, "step": { + "install_addon": { + "title": "La instalaci\u00f3n del complemento OpenZWave se ha iniciado" + }, "on_supervisor": { "data": { "use_addon": "Usar el complemento de supervisor de OpenZWave" diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json index 04c7cf2d50c..966e1a4065b 100644 --- a/homeassistant/components/ozw/translations/no.json +++ b/homeassistant/components/ozw/translations/no.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "Kunne ikke starte OpenZWave-tillegget. Sjekk konfigurasjonen." }, + "progress": { + "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter." + }, "step": { + "install_addon": { + "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet" + }, "on_supervisor": { "data": { "use_addon": "Bruk OpenZWave Supervisor-tillegget" diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json index b7a582faa08..b2f5ebd6e8e 100644 --- a/homeassistant/components/ozw/translations/ru.json +++ b/homeassistant/components/ozw/translations/ru.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenZWave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, + "progress": { + "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." + }, "step": { + "install_addon": { + "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor OpenZWave" diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 13fcadde01f..f4334e1d632 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "OpenZWave add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" }, + "progress": { + "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + }, "step": { + "install_addon": { + "title": "OpenZWave add-on \u5b89\u88dd\u5df2\u555f\u52d5" + }, "on_supervisor": { "data": { "use_addon": "\u4f7f\u7528 OpenZWave Supervisor add-on" diff --git a/homeassistant/components/sensor/translations/zh-Hans.json b/homeassistant/components/sensor/translations/zh-Hans.json index 44f3b415d4d..33a375c000a 100644 --- a/homeassistant/components/sensor/translations/zh-Hans.json +++ b/homeassistant/components/sensor/translations/zh-Hans.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "{entity_name} \u5f53\u524d\u7684\u7535\u6c60\u7535\u91cf", + "is_current": "{entity_name} \u5f53\u524d\u7684\u7535\u6d41", + "is_energy": "{entity_name} \u5f53\u524d\u7528\u7535\u91cf", "is_humidity": "{entity_name} \u5f53\u524d\u7684\u6e7f\u5ea6", "is_illuminance": "{entity_name} \u5f53\u524d\u7684\u5149\u7167\u5f3a\u5ea6", "is_power": "{entity_name} \u5f53\u524d\u7684\u529f\u7387", + "is_power_factor": "{entity_name} \u5f53\u524d\u7684\u529f\u7387\u56e0\u6570", "is_pressure": "{entity_name} \u5f53\u524d\u7684\u538b\u529b", "is_signal_strength": "{entity_name} \u5f53\u524d\u7684\u4fe1\u53f7\u5f3a\u5ea6", "is_temperature": "{entity_name} \u5f53\u524d\u7684\u6e29\u5ea6", "is_timestamp": "{entity_name} \u5f53\u524d\u7684\u65f6\u95f4\u6233", - "is_value": "{entity_name} \u5f53\u524d\u7684\u503c" + "is_value": "{entity_name} \u5f53\u524d\u7684\u503c", + "is_voltage": "{entity_name} \u5f53\u524d\u7684\u7535\u538b" }, "trigger_type": { "battery_level": "{entity_name} \u7684\u7535\u6c60\u7535\u91cf\u53d8\u5316", + "current": "{entity_name} \u7684\u7535\u6d41\u53d8\u5316", + "energy": "{entity_name} \u7684\u7528\u7535\u91cf\u53d8\u5316", "humidity": "{entity_name} \u7684\u6e7f\u5ea6\u53d8\u5316", "illuminance": "{entity_name} \u7684\u5149\u7167\u5f3a\u5ea6\u53d8\u5316", "power": "{entity_name} \u7684\u529f\u7387\u53d8\u5316", + "power_factor": "{entity_name} \u7684\u529f\u7387\u56e0\u6570\u53d8\u5316", "pressure": "{entity_name} \u7684\u538b\u529b\u53d8\u5316", "signal_strength": "{entity_name} \u7684\u4fe1\u53f7\u5f3a\u5ea6\u53d8\u5316", "temperature": "{entity_name} \u7684\u6e29\u5ea6\u53d8\u5316", "timestamp": "{entity_name} \u7684\u65f6\u95f4\u6233\u53d8\u5316", - "value": "{entity_name} \u7684\u503c\u53d8\u5316" + "value": "{entity_name} \u7684\u503c\u53d8\u5316", + "voltage": "{entity_name} \u7684\u7535\u538b\u53d8\u5316" } }, "state": { diff --git a/homeassistant/components/switch/translations/zh-Hans.json b/homeassistant/components/switch/translations/zh-Hans.json index 8820cb9e435..a18455aec6a 100644 --- a/homeassistant/components/switch/translations/zh-Hans.json +++ b/homeassistant/components/switch/translations/zh-Hans.json @@ -1,10 +1,13 @@ { "device_automation": { "action_type": { - "turn_off": "\u5173\u95ed {entity_name}" + "toggle": "\u5207\u6362 {entity_name} \u5f00\u5173", + "turn_off": "\u5173\u95ed {entity_name}", + "turn_on": "\u6253\u5f00 {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u5df2\u5173\u95ed" + "is_off": "{entity_name} \u5df2\u5173\u95ed", + "is_on": "{entity_name} \u5df2\u5f00\u542f" }, "trigger_type": { "turned_off": "{entity_name} \u88ab\u5173\u95ed", diff --git a/homeassistant/components/tag/translations/zh-Hans.json b/homeassistant/components/tag/translations/zh-Hans.json new file mode 100644 index 00000000000..5d655a9ada6 --- /dev/null +++ b/homeassistant/components/tag/translations/zh-Hans.json @@ -0,0 +1,3 @@ +{ + "title": "\u6807\u7b7e" +} \ No newline at end of file From 26892a629d9ea33dd6193a115f4789ca03787680 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 1 Dec 2020 17:23:08 -0700 Subject: [PATCH 333/430] Add ozw scene_instance to scene_activated (#43829) Co-authored-by: Martin Hjelmare --- homeassistant/components/ozw/__init__.py | 5 ++++- tests/components/ozw/test_scenes.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 5706b75efb8..2fe45018182 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -332,6 +332,7 @@ async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): """Handle a (central) scene activation message.""" node_id = scene_value.node.id + ozw_instance_id = scene_value.ozw_instance.id scene_id = scene_value.index scene_label = scene_value.label if scene_value.command_class == CommandClass.SCENE_ACTIVATION: @@ -346,7 +347,8 @@ def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): scene_value_id = scene_value.value["Selected_id"] _LOGGER.debug( - "[SCENE_ACTIVATED] node_id: %s - scene_id: %s - scene_value_id: %s", + "[SCENE_ACTIVATED] ozw_instance: %s - node_id: %s - scene_id: %s - scene_value_id: %s", + ozw_instance_id, node_id, scene_id, scene_value_id, @@ -355,6 +357,7 @@ def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): hass.bus.async_fire( const.EVENT_SCENE_ACTIVATED, { + const.ATTR_INSTANCE_ID: ozw_instance_id, const.ATTR_NODE_ID: node_id, const.ATTR_SCENE_ID: scene_id, const.ATTR_SCENE_LABEL: scene_label, diff --git a/tests/components/ozw/test_scenes.py b/tests/components/ozw/test_scenes.py index 2d776b7faf4..1c510d58a3c 100644 --- a/tests/components/ozw/test_scenes.py +++ b/tests/components/ozw/test_scenes.py @@ -86,3 +86,4 @@ async def test_scenes(hass, generic_data, sent_messages): assert events[1].data["scene_id"] == 1 assert events[1].data["scene_label"] == "Scene 1" assert events[1].data["scene_value_label"] == "Pressed 1 Time" + assert events[1].data["instance_id"] == 1 From 3efda93875708aee16bd676b7cb7bea6615c72d4 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Wed, 2 Dec 2020 05:50:09 +0100 Subject: [PATCH 334/430] Support more edl21 devices and sensors (#43603) * edl21: Handle 1-0:96.50.1*1 and 1-0:96.1.0*255 * edl21: Use 1-0:96.1.0*255 as alternative electricity ID * edl21: Fix copy-paste error in comments * edl21: Add active amperage sensors * edl21: Bump pysml to 0.0.3 to fix format of electricity ID --- homeassistant/components/edl21/manifest.json | 2 +- homeassistant/components/edl21/sensor.py | 22 +++++++++++++++++--- requirements_all.txt | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index 3e469e44601..c3b65c3b352 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,6 +2,6 @@ "domain": "edl21", "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", - "requirements": ["pysml==0.0.2"], + "requirements": ["pysml==0.0.3"], "codeowners": ["@mtdcr"] } diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 5eb7542fed9..dc0f51abe61 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -77,20 +77,36 @@ class EDL21: # D=7: Instantaneous value # E=0: Total "1-0:16.7.0*255": "Sum active instantaneous power", + # C=31: Active amperage L1 + # D=7: Instantaneous value + # E=0: Total + "1-0:31.7.0*255": "L1 active instantaneous amperage", # C=36: Active power L1 # D=7: Instantaneous value # E=0: Total "1-0:36.7.0*255": "L1 active instantaneous power", - # C=56: Active power L1 + # C=51: Active amperage L2 + # D=7: Instantaneous value + # E=0: Total + "1-0:51.7.0*255": "L2 active instantaneous amperage", + # C=56: Active power L2 # D=7: Instantaneous value # E=0: Total "1-0:56.7.0*255": "L2 active instantaneous power", - # C=76: Active power L1 + # C=71: Active amperage L3 + # D=7: Instantaneous value + # E=0: Total + "1-0:71.7.0*255": "L3 active instantaneous amperage", + # C=76: Active power L3 # D=7: Instantaneous value # E=0: Total "1-0:76.7.0*255": "L3 active instantaneous power", + # C=96: Electricity-related service entries + "1-0:96.1.0*255": "Metering point ID 1", } _OBIS_BLACKLIST = { + # C=96: Electricity-related service entries + "1-0:96.50.1*1", # Manufacturer specific # A=129: Manufacturer specific "129-129:199.130.3*255", # Iskraemeco: Manufacturer "129-129:199.130.5*255", # Iskraemeco: Public Key @@ -115,7 +131,7 @@ class EDL21: electricity_id = None for telegram in message_body.get("valList", []): - if telegram.get("objName") == "1-0:0.0.9*255": + if telegram.get("objName") in ("1-0:0.0.9*255", "1-0:96.1.0*255"): electricity_id = telegram.get("value") break diff --git a/requirements_all.txt b/requirements_all.txt index 0f5362ff7dd..e061cc956fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1692,7 +1692,7 @@ pysmartthings==0.7.6 pysmarty==0.8 # homeassistant.components.edl21 -pysml==0.0.2 +pysml==0.0.3 # homeassistant.components.snmp pysnmp==4.4.12 From d93687b5ac58c1009d270b766cab9bc76597edb1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Dec 2020 09:30:49 +0100 Subject: [PATCH 335/430] Update area and target selectors add sequence selector (#43831) Co-authored-by: Paulus Schoutsen --- .../automation/blueprints/motion_light.yaml | 1 + homeassistant/helpers/selector.py | 45 ++++++++++++++++++- tests/helpers/test_selector.py | 42 ++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index cd6e5c7b281..8b17fbe91fe 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -23,6 +23,7 @@ blueprint: number: min: 0 max: 3600 + unit_of_measurement: seconds # If motion is detected within the delay, # we restart the script. diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index b4a90212bce..5cc7ada1bc5 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -79,7 +79,24 @@ class DeviceSelector(Selector): class AreaSelector(Selector): """Selector of a single area.""" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): vol.Schema( + { + vol.Optional("domain"): str, + vol.Optional("device_class"): str, + vol.Optional("integration"): str, + } + ), + vol.Optional("device"): vol.Schema( + { + vol.Optional("integration"): str, + vol.Optional("manufacturer"): str, + vol.Optional("model"): str, + } + ), + } + ) @SELECTORS.register("number") @@ -120,4 +137,28 @@ class TargetSelector(Selector): Value should follow cv.ENTITY_SERVICE_FIELDS format. """ - CONFIG_SCHEMA = vol.Schema({"entity": {"domain": str, "device_class": str}}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): vol.Schema( + { + vol.Optional("domain"): str, + vol.Optional("device_class"): str, + vol.Optional("integration"): str, + } + ), + vol.Optional("device"): vol.Schema( + { + vol.Optional("integration"): str, + vol.Optional("manufacturer"): str, + vol.Optional("model"): str, + } + ), + } + ) + + +@SELECTORS.register("action") +class ActionSelector(Selector): + """Selector of an action sequence (script syntax).""" + + CONFIG_SCHEMA = vol.Schema({}) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 531d36e4f50..86ee6078e87 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -79,7 +79,24 @@ def test_entity_selector_schema(schema): @pytest.mark.parametrize( "schema", - ({},), + ( + {}, + {"entity": {}}, + {"entity": {"domain": "light"}}, + {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + {"device": {"integration": "demo", "model": "mock-model"}}, + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, + ), ) def test_area_selector_schema(schema): """Test area selector.""" @@ -126,8 +143,29 @@ def test_time_selector_schema(schema): {"entity": {}}, {"entity": {"domain": "light"}}, {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + {"device": {"integration": "demo", "model": "mock-model"}}, + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, ), ) def test_target_selector_schema(schema): - """Test entity selector.""" + """Test target selector.""" selector.validate_selector({"target": schema}) + + +@pytest.mark.parametrize( + "schema", + ({},), +) +def test_action_selector_schema(schema): + """Test action sequence selector.""" + selector.validate_selector({"action": schema}) From cf286d1c513b07a3268e41f45d1a96f3dece3b4c Mon Sep 17 00:00:00 2001 From: etheralm Date: Wed, 2 Dec 2020 09:51:46 +0100 Subject: [PATCH 336/430] Bump libpurecool to 0.6.4 (#43779) --- CODEOWNERS | 1 - homeassistant/components/dyson/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4b687e071ea..6b2a8686dad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -117,7 +117,6 @@ homeassistant/components/dunehd/* @bieniu homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 -homeassistant/components/dyson/* @etheralm homeassistant/components/eafm/* @Jc2k homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 94a29d1615d..4678b1ad598 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -2,7 +2,7 @@ "domain": "dyson", "name": "Dyson", "documentation": "https://www.home-assistant.io/integrations/dyson", - "requirements": ["libpurecool==0.6.3"], + "requirements": ["libpurecool==0.6.4"], "after_dependencies": ["zeroconf"], - "codeowners": ["@etheralm"] + "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index e061cc956fb..9d104366e5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -856,7 +856,7 @@ konnected==1.2.0 lakeside==0.12 # homeassistant.components.dyson -libpurecool==0.6.3 +libpurecool==0.6.4 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be2fa0ec0f9..7f195c8d538 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -441,7 +441,7 @@ keyrings.alt==3.4.0 konnected==1.2.0 # homeassistant.components.dyson -libpurecool==0.6.3 +libpurecool==0.6.4 # homeassistant.components.mikrotik librouteros==3.0.0 From 891edec73bc63b1e0d7ff09dcd663316ac98ae33 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 2 Dec 2020 03:12:48 -0600 Subject: [PATCH 337/430] Add test for is_internal_request (#43841) --- tests/helpers/test_network.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index c470fbd7834..3330c42d9fc 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -12,6 +12,7 @@ from homeassistant.helpers.network import ( _get_internal_url, _get_request_host, get_url, + is_internal_request, ) from tests.async_mock import Mock, patch @@ -860,3 +861,40 @@ async def test_get_current_request_url_with_known_host( "homeassistant.helpers.network._get_request_host", return_value="unknown.local" ), pytest.raises(NoURLAvailableError): get_url(hass, require_current_request=True) + + +async def test_is_internal_request(hass: HomeAssistant): + """Test if accessing an instance on its internal URL.""" + # Test with internal URL: http://example.local:8123 + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + + assert hass.config.internal_url == "http://example.local:8123" + assert not is_internal_request(hass) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="example.local" + ): + assert is_internal_request(hass) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.local", + ): + assert not is_internal_request(hass) + + # Test with internal URL: http://192.168.0.1:8123 + await async_process_ha_core_config( + hass, + {"internal_url": "http://192.168.0.1:8123"}, + ) + + assert hass.config.internal_url == "http://192.168.0.1:8123" + assert not is_internal_request(hass) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1" + ): + assert is_internal_request(hass) From fec0d26d55b1d1bb2cf41495ebb009336ab98213 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 2 Dec 2020 10:20:12 +0100 Subject: [PATCH 338/430] Upgrade TwitterAPI to 2.6.2.1 (#43833) --- homeassistant/components/twitter/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 044151094da..c497ebfa6f5 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,6 +2,6 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.5.13"], + "requirements": ["TwitterAPI==2.6.2.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d104366e5e..5ffe4e27224 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.5.13 +TwitterAPI==2.6.2.1 # homeassistant.components.tof # VL53L1X2==0.1.5 From 834f3603bcba2a873a58eb0babcf21c043a80465 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Dec 2020 10:32:25 +0100 Subject: [PATCH 339/430] Correct service not found exception message (#43846) --- homeassistant/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 015e81f2ca0..e37f68a07bf 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -80,4 +80,4 @@ class ServiceNotFound(HomeAssistantError): def __str__(self) -> str: """Return string representation.""" - return f"Unable to find service {self.domain}/{self.service}" + return f"Unable to find service {self.domain}.{self.service}" From bbe987432044b30cde2e957d3f2758b235652818 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 2 Dec 2020 10:47:45 +0100 Subject: [PATCH 340/430] Upgrade pylast to 4.0.0 (#43830) --- homeassistant/components/lastfm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 1bc38cb0359..adcd7e8d3d0 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -2,6 +2,6 @@ "domain": "lastfm", "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": ["pylast==3.3.0"], + "requirements": ["pylast==4.0.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 5ffe4e27224..382ead0322c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lastfm -pylast==3.3.0 +pylast==4.0.0 # homeassistant.components.launch_library pylaunches==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f195c8d538..ab1012beb1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -743,7 +743,7 @@ pykira==0.1.1 pykodi==0.2.1 # homeassistant.components.lastfm -pylast==3.3.0 +pylast==4.0.0 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 From 93055884b7b4e774bcabd67b2229532ba0108170 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 2 Dec 2020 10:49:50 +0100 Subject: [PATCH 341/430] Bump pypck to 0.7.7 (#43824) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 6811e086ecc..f07c4d9c646 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,6 +2,6 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.6"], + "requirements": ["pypck==0.7.7"], "codeowners": ["@alengwenus"] } diff --git a/requirements_all.txt b/requirements_all.txt index 382ead0322c..f475a25e19b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.6 +pypck==0.7.7 # homeassistant.components.pjlink pypjlink2==1.2.1 From 648f9e100d5702d6e7a79020cac17c74797d3b89 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Dec 2020 10:53:26 +0100 Subject: [PATCH 342/430] Use light turn on service (#43847) --- .../components/automation/blueprints/motion_light.yaml | 4 ++-- tests/components/automation/test_blueprint.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index 8b17fbe91fe..c10d3691e6b 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -37,7 +37,7 @@ trigger: to: "on" action: - - service: homeassistant.turn_on + - service: light.turn_on target: !input light_target - wait_for_trigger: platform: state @@ -45,5 +45,5 @@ action: from: "on" to: "off" - delay: !input no_motion_wait - - service: homeassistant.turn_off + - service: light.turn_off target: !input light_target diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index a455d1de5b5..0a651b501c5 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -131,8 +131,8 @@ async def test_motion_light(hass): }, ) - turn_on_calls = async_mock_service(hass, "homeassistant", "turn_on") - turn_off_calls = async_mock_service(hass, "homeassistant", "turn_off") + turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_off_calls = async_mock_service(hass, "light", "turn_off") # Turn on motion hass.states.async_set("binary_sensor.kitchen", "on") From f3bb243b1da24f82eee8f4d1bb3393065ceca6e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Dec 2020 11:18:08 +0100 Subject: [PATCH 343/430] Do not warn for weak referenced entities (#43848) --- .../components/homeassistant/__init__.py | 37 ++++++++++++------- tests/components/homeassistant/test_init.py | 21 ++++++----- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index f1b8d4e87d6..c2ee40b7d43 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_extract_entity_ids +from homeassistant.helpers.service import async_extract_referenced_entity_ids _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN @@ -37,39 +37,37 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" - entity_ids = await async_extract_entity_ids(hass, service) + referenced = await async_extract_referenced_entity_ids(hass, service) + all_referenced = referenced.referenced | referenced.indirectly_referenced # Generic turn on/off method requires entity id - if not entity_ids: + if not all_referenced: _LOGGER.error( - "homeassistant/%s cannot be called without entity_id", service.service + "homeassistant.%s cannot be called without a target", service.service ) return # Group entity_ids by domain. groupby requires sorted data. by_domain = it.groupby( - sorted(entity_ids), lambda item: ha.split_entity_id(item)[0] + sorted(all_referenced), lambda item: ha.split_entity_id(item)[0] ) tasks = [] + unsupported_entities = set() for domain, ent_ids in by_domain: # This leads to endless loop. if domain == DOMAIN: _LOGGER.warning( - "Called service homeassistant.%s with invalid entity IDs %s", + "Called service homeassistant.%s with invalid entities %s", service.service, ", ".join(ent_ids), ) continue - # We want to block for all calls and only return when all calls - # have been processed. If a service does not exist it causes a 10 - # second delay while we're blocking waiting for a response. - # But services can be registered on other HA instances that are - # listening to the bus too. So as an in between solution, we'll - # block only if the service is defined in the current HA instance. - blocking = hass.services.has_service(domain, service.service) + if not hass.services.has_service(domain, service.service): + unsupported_entities.update(set(ent_ids) & referenced.referenced) + continue # Create a new dict for this call data = dict(service.data) @@ -79,10 +77,21 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: tasks.append( hass.services.async_call( - domain, service.service, data, blocking, context=service.context + domain, + service.service, + data, + blocking=True, + context=service.context, ) ) + if unsupported_entities: + _LOGGER.warning( + "The service homeassistant.%s does not support entities %s", + service.service, + ", ".join(sorted(unsupported_entities)), + ) + if tasks: await asyncio.gather(*tasks) diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 3ad3ef76483..ca2f116a06a 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -248,7 +248,7 @@ class TestComponentsCore(unittest.TestCase): assert not mock_stop.called -async def test_turn_on_to_not_block_for_domains_without_service(hass): +async def test_turn_on_skips_domains_without_service(hass, caplog): """Test if turn_on is blocking domain with no service.""" await async_setup_component(hass, "homeassistant", {}) async_mock_service(hass, "light", SERVICE_TURN_ON) @@ -261,7 +261,7 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): service_call = ha.ServiceCall( "homeassistant", "turn_on", - {"entity_id": ["light.test", "sensor.bla", "light.bla"]}, + {"entity_id": ["light.test", "sensor.bla", "binary_sensor.blub", "light.bla"]}, ) service = hass.services._services["homeassistant"]["turn_on"] @@ -271,18 +271,19 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): ) as mock_call: await service.job.target(service_call) - assert mock_call.call_count == 2 + assert mock_call.call_count == 1 assert mock_call.call_args_list[0][0] == ( "light", "turn_on", {"entity_id": ["light.bla", "light.test"]}, - True, ) - assert mock_call.call_args_list[1][0] == ( - "sensor", - "turn_on", - {"entity_id": ["sensor.bla"]}, - False, + assert mock_call.call_args_list[0][1] == { + "blocking": True, + "context": service_call.context, + } + assert ( + "The service homeassistant.turn_on does not support entities binary_sensor.blub, sensor.bla" + in caplog.text ) @@ -381,6 +382,6 @@ async def test_not_allowing_recursion(hass, caplog): blocking=True, ) assert ( - f"Called service homeassistant.{service} with invalid entity IDs homeassistant.light" + f"Called service homeassistant.{service} with invalid entities homeassistant.light" in caplog.text ), service From 6c9c280bbb54bc30034dfda8821e072cc12841b6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Dec 2020 13:07:04 +0100 Subject: [PATCH 344/430] Migrate notify-leaving-zone to use mobile app device action (#43832) --- .../blueprints/notify_leaving_zone.yaml | 15 +++-- homeassistant/components/mobile_app/util.py | 3 + tests/components/automation/test_blueprint.py | 57 +++++++++++-------- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 178c3222c0a..5b64a090a07 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -13,8 +13,12 @@ blueprint: selector: entity: domain: zone - notify_service: - name: The notify service to use + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app trigger: platform: state @@ -29,6 +33,7 @@ condition: value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" action: - - service: !input notify_service - data: - message: "{{ trigger.to_state.name }} has left {{ zone_state }}" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ trigger.to_state.name }} has left {{ zone_state }}" diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index d9a5f1643c7..60dfe242e04 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -20,6 +20,9 @@ if TYPE_CHECKING: @callback def webhook_id_from_device_id(hass, device_id: str) -> Optional[str]: """Get webhook ID from device ID.""" + if DOMAIN not in hass.data: + return None + for cur_webhook_id, cur_device in hass.data[DOMAIN][DATA_DEVICES].items(): if cur_device.id == device_id: return cur_webhook_id diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 0a651b501c5..568087840c4 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -66,45 +66,54 @@ async def test_notify_leaving_zone(hass): "input": { "person_entity": "person.test_person", "zone_entity": "zone.school", - "notify_service": "notify.test_service", + "notify_device": "abcdefgh", }, } } }, ) - calls = async_mock_service(hass, "notify", "test_service") + with patch( + "homeassistant.components.mobile_app.device_action.async_call_action_from_config" + ) as mock_call_action: + # Leaving zone to no zone + set_person_state("not_home") + await hass.async_block_till_done() - # Leaving zone to no zone - set_person_state("not_home") - await hass.async_block_till_done() + assert len(mock_call_action.mock_calls) == 1 + _hass, config, variables, _context = mock_call_action.mock_calls[0][1] + message_tpl = config.pop("message") + assert config == { + "domain": "mobile_app", + "type": "notify", + "device_id": "abcdefgh", + } + message_tpl.hass = hass + assert message_tpl.async_render(variables) == "Paulus has left School" - assert len(calls) == 1 - assert calls[0].data["message"] == "Paulus has left School" + # Should not increase when we go to another zone + set_person_state("bla") + await hass.async_block_till_done() - # Should not increase when we go to another zone - set_person_state("bla") - await hass.async_block_till_done() + assert len(mock_call_action.mock_calls) == 1 - assert len(calls) == 1 + # Should not increase when we go into the zone + set_person_state("School") + await hass.async_block_till_done() - # Should not increase when we go into the zone - set_person_state("School") - await hass.async_block_till_done() + assert len(mock_call_action.mock_calls) == 1 - assert len(calls) == 1 + # Should not increase when we move in the zone + set_person_state("School", {"extra_key": "triggers change with same state"}) + await hass.async_block_till_done() - # Should not increase when we move in the zone - set_person_state("School", {"extra_key": "triggers change with same state"}) - await hass.async_block_till_done() + assert len(mock_call_action.mock_calls) == 1 - assert len(calls) == 1 + # Should increase when leaving zone for another zone + set_person_state("Just Outside School") + await hass.async_block_till_done() - # Should increase when leaving zone for another zone - set_person_state("Just Outside School") - await hass.async_block_till_done() - - assert len(calls) == 2 + assert len(mock_call_action.mock_calls) == 2 async def test_motion_light(hass): From f744f7c34ee725a268c21efba136a7c98aab4159 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Wed, 2 Dec 2020 15:50:48 +0300 Subject: [PATCH 345/430] Add new number entity integration (#42735) Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/number.py | 130 ++++++++++++++++++ homeassistant/components/number/__init__.py | 103 ++++++++++++++ homeassistant/components/number/const.py | 14 ++ homeassistant/components/number/manifest.json | 7 + homeassistant/components/number/services.yaml | 11 ++ setup.cfg | 3 +- tests/components/demo/test_number.py | 97 +++++++++++++ tests/components/number/__init__.py | 1 + tests/components/number/test_init.py | 39 ++++++ 11 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/demo/number.py create mode 100644 homeassistant/components/number/__init__.py create mode 100644 homeassistant/components/number/const.py create mode 100644 homeassistant/components/number/manifest.json create mode 100644 homeassistant/components/number/services.yaml create mode 100644 tests/components/demo/test_number.py create mode 100644 tests/components/number/__init__.py create mode 100644 tests/components/number/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 6b2a8686dad..2ca8a0a0f85 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -308,6 +308,7 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pschmitt @pvizeli homeassistant/components/numato/* @clssn +homeassistant/components/number/* @home-assistant/core @Shulyaka homeassistant/components/nut/* @bdraco homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index eea613cc401..09c3d27a1bc 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -19,6 +19,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "light", "lock", "media_player", + "number", "sensor", "switch", "vacuum", diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py new file mode 100644 index 00000000000..a8b9cb0ac4d --- /dev/null +++ b/homeassistant/components/demo/number.py @@ -0,0 +1,130 @@ +"""Demo platform that offers a fake Number entity.""" +import voluptuous as vol + +from homeassistant.components.number import NumberEntity +from homeassistant.const import DEVICE_DEFAULT_NAME + +from . import DOMAIN + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the demo Number entity.""" + async_add_entities( + [ + DemoNumber( + "volume1", + "volume", + 42.0, + "mdi:volume-high", + False, + ), + DemoNumber( + "pwm1", + "PWM 1", + 42.0, + "mdi:square-wave", + False, + 0.0, + 1.0, + 0.01, + ), + ] + ) + + +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 DemoNumber(NumberEntity): + """Representation of a demo Number entity.""" + + def __init__( + self, + unique_id, + name, + state, + icon, + assumed, + min_value=None, + max_value=None, + step=None, + ): + """Initialize the Demo Number entity.""" + self._unique_id = unique_id + self._name = name or DEVICE_DEFAULT_NAME + self._state = state + self._icon = icon + self._assumed = assumed + self._min_value = min_value + self._max_value = max_value + self._step = step + + @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 Number entity.""" + return False + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def assumed_state(self): + """Return if the state is based on assumptions.""" + return self._assumed + + @property + def state(self): + """Return the current value.""" + return self._state + + @property + def min_value(self): + """Return the minimum value.""" + return self._min_value or super().min_value + + @property + def max_value(self): + """Return the maximum value.""" + return self._max_value or super().max_value + + @property + def step(self): + """Return the value step.""" + return self._step or super().step + + async def async_set_value(self, value): + """Update the current value.""" + num_value = float(value) + + if num_value < self.min_value or num_value > self.max_value: + raise vol.Invalid( + f"Invalid value for {self.entity_id}: {value} (range {self.min_value} - {self.max_value})" + ) + + self._state = num_value + self.async_write_ha_state() diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py new file mode 100644 index 00000000000..2fd04943e4d --- /dev/null +++ b/homeassistant/components/number/__init__.py @@ -0,0 +1,103 @@ +"""Component to allow numeric input for platforms.""" +from datetime import timedelta +import logging +from typing import Any, Dict + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +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.typing import ConfigType, HomeAssistantType + +from .const import ( + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, + ATTR_VALUE, + DEFAULT_MAX_VALUE, + DEFAULT_MIN_VALUE, + DEFAULT_STEP, + DOMAIN, + SERVICE_SET_VALUE, +) + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up Number entities.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + "async_set_value", + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore + + +class NumberEntity(Entity): + """Representation of a Number entity.""" + + @property + def capability_attributes(self) -> Dict[str, Any]: + """Return capability attributes.""" + return { + ATTR_MIN: self.min_value, + ATTR_MAX: self.max_value, + ATTR_STEP: self.step, + } + + @property + def min_value(self) -> float: + """Return the minimum value.""" + return DEFAULT_MIN_VALUE + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return DEFAULT_MAX_VALUE + + @property + def step(self) -> float: + """Return the increment/decrement step.""" + step = DEFAULT_STEP + value_range = abs(self.max_value - self.min_value) + if value_range != 0: + while value_range <= step: + step /= 10.0 + return step + + def set_value(self, value: float) -> None: + """Set new value.""" + raise NotImplementedError() + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + assert self.hass is not None + await self.hass.async_add_executor_job(self.set_value, value) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py new file mode 100644 index 00000000000..2aa8075cba3 --- /dev/null +++ b/homeassistant/components/number/const.py @@ -0,0 +1,14 @@ +"""Provides the constants needed for the component.""" + +ATTR_VALUE = "value" +ATTR_MIN = "min" +ATTR_MAX = "max" +ATTR_STEP = "step" + +DEFAULT_MIN_VALUE = 0.0 +DEFAULT_MAX_VALUE = 100.0 +DEFAULT_STEP = 1.0 + +DOMAIN = "number" + +SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/number/manifest.json b/homeassistant/components/number/manifest.json new file mode 100644 index 00000000000..549494fa3f5 --- /dev/null +++ b/homeassistant/components/number/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "number", + "name": "Number", + "documentation": "https://www.home-assistant.io/integrations/number", + "codeowners": ["@home-assistant/core", "@Shulyaka"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml new file mode 100644 index 00000000000..d18416f9974 --- /dev/null +++ b/homeassistant/components/number/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available Number entity services + +set_value: + description: Set the value of a Number entity. + fields: + entity_id: + description: Entity ID of the Number to set the new value. + example: number.volume + value: + description: The target value the entity should be set to. + example: 42 diff --git a/setup.cfg b/setup.cfg index 6ff4e1abb12..de5092dcecf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,8 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,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.huawei_lte.*,homeassistant.components.hyperion.*,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.*,tests.components.hyperion.*] + +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,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.huawei_lte.*,homeassistant.components.hyperion.*,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.number.*,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.*,tests.components.hyperion.*] strict = true ignore_errors = false warn_unreachable = true diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py new file mode 100644 index 00000000000..711332b7817 --- /dev/null +++ b/tests/components/demo/test_number.py @@ -0,0 +1,97 @@ +"""The tests for the demo number component.""" + +import pytest +import voluptuous as vol + +from homeassistant.components.number.const import ( + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, + ATTR_VALUE, + DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.setup import async_setup_component + +ENTITY_VOLUME = "number.volume" +ENTITY_PWM = "number.pwm_1" + + +@pytest.fixture(autouse=True) +async def setup_demo_number(hass): + """Initialize setup demo Number entity.""" + assert await async_setup_component(hass, DOMAIN, {"number": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass): + """Test the initial parameters.""" + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "42.0" + + +def test_default_setup_params(hass): + """Test the setup with default parameters.""" + state = hass.states.get(ENTITY_VOLUME) + assert state.attributes.get(ATTR_MIN) == 0.0 + assert state.attributes.get(ATTR_MAX) == 100.0 + assert state.attributes.get(ATTR_STEP) == 1.0 + + state = hass.states.get(ENTITY_PWM) + assert state.attributes.get(ATTR_MIN) == 0.0 + assert state.attributes.get(ATTR_MAX) == 1.0 + assert state.attributes.get(ATTR_STEP) == 0.01 + + +async def test_set_value_bad_attr(hass): + """Test setting the value without required attribute.""" + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "42.0" + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: None, ATTR_ENTITY_ID: ENTITY_VOLUME}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "42.0" + + +async def test_set_value_bad_range(hass): + """Test setting the value out of range.""" + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "42.0" + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: 1024, ATTR_ENTITY_ID: ENTITY_VOLUME}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "42.0" + + +async def test_set_set_value(hass): + """Test the setting of the value.""" + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "42.0" + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: 23, ATTR_ENTITY_ID: ENTITY_VOLUME}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_VOLUME) + assert state.state == "23.0" diff --git a/tests/components/number/__init__.py b/tests/components/number/__init__.py new file mode 100644 index 00000000000..e2e32e7a355 --- /dev/null +++ b/tests/components/number/__init__.py @@ -0,0 +1 @@ +"""The tests for Number integration.""" diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py new file mode 100644 index 00000000000..6037bde5afd --- /dev/null +++ b/tests/components/number/test_init.py @@ -0,0 +1,39 @@ +"""The tests for the Number component.""" +from unittest.mock import MagicMock + +from homeassistant.components.number import NumberEntity + + +class MockNumberEntity(NumberEntity): + """Mock NumberEntity device to use in tests.""" + + @property + def max_value(self) -> float: + """Return the max value.""" + return 1.0 + + @property + def state(self): + """Return the current value.""" + return "0.5" + + +async def test_step(hass): + """Test the step calculation.""" + number = NumberEntity() + assert number.step == 1.0 + + number_2 = MockNumberEntity() + assert number_2.step == 0.1 + + +async def test_sync_set_value(hass): + """Test if async set_value calls sync set_value.""" + number = NumberEntity() + number.hass = hass + + number.set_value = MagicMock() + await number.async_set_value(42) + + assert number.set_value.called + assert number.set_value.call_args[0][0] == 42 From 6fadc3e1400088e6288cbdb0181c3a9a89c61c41 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 2 Dec 2020 14:24:47 +0100 Subject: [PATCH 346/430] Make "invalid password" error message clearer (#43853) --- .../components/config/auth_provider_homeassistant.py | 4 +++- tests/components/config/test_auth_provider_homeassistant.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 44ac6f23e2d..a8421c4c0f6 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -124,7 +124,9 @@ async def websocket_change_password(hass, connection, msg): try: await provider.async_validate_login(username, msg["current_password"]) except auth_ha.InvalidAuth: - connection.send_error(msg["id"], "invalid_password", "Invalid password") + connection.send_error( + msg["id"], "invalid_current_password", "Invalid current password" + ) return await provider.async_change_password(username, msg["new_password"]) diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 19568ff450b..6af3e6507d5 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -290,7 +290,7 @@ async def test_change_password_wrong_pw( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "invalid_password" + assert result["error"]["code"] == "invalid_current_password" with pytest.raises(prov_ha.InvalidAuth): await auth_provider.async_validate_login("test-user", "new-pass") From 15b50575695876e962e26e8f9f9f35c2e6303779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 2 Dec 2020 14:34:17 +0100 Subject: [PATCH 347/430] Increase timeout for snapshot upload (#43851) --- homeassistant/components/hassio/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 2c1445dd456..2aa05ae6ab4 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -77,6 +77,7 @@ class HassIOView(HomeAssistantView): This method is a coroutine. """ read_timeout = _get_timeout(path) + client_timeout = 10 data = None headers = _init_header(request) if path == "snapshots/new/upload": @@ -89,9 +90,10 @@ class HassIOView(HomeAssistantView): request._client_max_size = ( # pylint: disable=protected-access MAX_UPLOAD_SIZE ) + client_timeout = 300 try: - with async_timeout.timeout(10): + with async_timeout.timeout(client_timeout): data = await request.read() method = getattr(self._websession, request.method.lower()) From 06626af337fbb5f3974238ef505edc43bec8084a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Dec 2020 14:55:47 +0100 Subject: [PATCH 348/430] Guard for when refreshing token fails (#43855) --- homeassistant/components/spotify/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 10fe70c611e..e28e1fcf315 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,5 +1,6 @@ """The spotify integration.""" +import aiohttp from spotipy import Spotify, SpotifyException import voluptuous as vol @@ -62,7 +63,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) - await session.async_ensure_token_valid() + + try: + await session.async_ensure_token_valid() + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err + spotify = Spotify(auth=session.token["access_token"]) try: From 55edc9f8580aea07618523e3aa72c327be175627 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Dec 2020 15:11:20 +0100 Subject: [PATCH 349/430] Cleanup unique_id on onewire integration (#43783) * Update construction of unique_id * Move shared logic into OneWireBaseEntity --- .../components/onewire/binary_sensor.py | 25 +++++++------ .../components/onewire/onewire_entities.py | 21 +++++++---- homeassistant/components/onewire/sensor.py | 35 +++++++++---------- homeassistant/components/onewire/switch.py | 25 +++++++------ 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 031dd984702..86b584c998c 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -94,29 +94,28 @@ def get_entities(onewirehub: OneWireHub): for device in onewirehub.devices: family = device["family"] device_type = device["type"] - sensor_id = os.path.split(os.path.split(device["path"])[0])[1] + device_id = os.path.split(os.path.split(device["path"])[0])[1] if family not in DEVICE_BINARY_SENSORS: continue device_info = { - "identifiers": {(DOMAIN, sensor_id)}, + "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, - "name": sensor_id, + "name": device_id, } - for device_sensor in DEVICE_BINARY_SENSORS[family]: - device_file = os.path.join( - os.path.split(device["path"])[0], device_sensor["path"] + for entity_specs in DEVICE_BINARY_SENSORS[family]: + entity_path = os.path.join( + os.path.split(device["path"])[0], entity_specs["path"] ) entities.append( OneWireProxyBinarySensor( - sensor_id, - device_file, - device_sensor["type"], - device_sensor["name"], - device_info, - device_sensor.get("default_disabled", False), - onewirehub.owproxy, + device_id=device_id, + device_name=device_id, + device_info=device_info, + entity_path=entity_path, + entity_specs=entity_specs, + owproxy=onewirehub.owproxy, ) ) diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index d16a2e9b493..9238bb5d32c 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -28,6 +28,7 @@ class OneWireBaseEntity(Entity): entity_name: str = None, device_info=None, default_disabled: bool = False, + unique_id: str = None, ): """Initialize the entity.""" self._name = f"{name} {entity_name or entity_type.capitalize()}" @@ -39,6 +40,7 @@ class OneWireBaseEntity(Entity): self._state = None self._value_raw = None self._default_disabled = default_disabled + self._unique_id = unique_id or device_file @property def name(self) -> Optional[str]: @@ -63,7 +65,7 @@ class OneWireBaseEntity(Entity): @property def unique_id(self) -> Optional[str]: """Return a unique ID.""" - return self._device_file + return self._unique_id @property def device_info(self) -> Optional[Dict[str, Any]]: @@ -81,17 +83,22 @@ class OneWireProxyEntity(OneWireBaseEntity): def __init__( self, - name: str, - device_file: str, - entity_type: str, - entity_name: str, + device_id: str, + device_name: str, device_info: Dict[str, Any], - default_disabled: bool, + entity_path: str, + entity_specs: Dict[str, Any], owproxy: protocol._Proxy, ): """Initialize the sensor.""" super().__init__( - name, device_file, entity_type, entity_name, device_info, default_disabled + name=device_name, + device_file=entity_path, + entity_type=entity_specs["type"], + entity_name=entity_specs["name"], + device_info=device_info, + default_disabled=entity_specs.get("default_disabled", False), + unique_id=f"/{device_id}/{entity_specs['path']}", ) self._owproxy = owproxy diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index f908c1ada2d..98090dc949f 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -244,7 +244,7 @@ def get_entities(onewirehub: OneWireHub, config): for device in onewirehub.devices: family = device["family"] device_type = device["type"] - sensor_id = os.path.split(os.path.split(device["path"])[0])[1] + device_id = os.path.split(os.path.split(device["path"])[0])[1] dev_type = "std" if "EF" in family: dev_type = "HobbyBoard" @@ -254,38 +254,37 @@ def get_entities(onewirehub: OneWireHub, config): _LOGGER.warning( "Ignoring unknown family (%s) of sensor found for device: %s", family, - sensor_id, + device_id, ) continue device_info = { - "identifiers": {(DOMAIN, sensor_id)}, + "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, - "name": sensor_id, + "name": device_id, } - for device_sensor in hb_info_from_type(dev_type)[family]: - if device_sensor["type"] == SENSOR_TYPE_MOISTURE: - s_id = device_sensor["path"].split(".")[1] + for entity_specs in hb_info_from_type(dev_type)[family]: + if entity_specs["type"] == SENSOR_TYPE_MOISTURE: + s_id = entity_specs["path"].split(".")[1] is_leaf = int( onewirehub.owproxy.read( f"{device['path']}moisture/is_leaf.{s_id}" ).decode() ) if is_leaf: - device_sensor["type"] = SENSOR_TYPE_WETNESS - device_sensor["name"] = f"Wetness {s_id}" - device_file = os.path.join( - os.path.split(device["path"])[0], device_sensor["path"] + entity_specs["type"] = SENSOR_TYPE_WETNESS + entity_specs["name"] = f"Wetness {s_id}" + entity_path = os.path.join( + os.path.split(device["path"])[0], entity_specs["path"] ) entities.append( OneWireProxySensor( - device_names.get(sensor_id, sensor_id), - device_file, - device_sensor["type"], - device_sensor["name"], - device_info, - device_sensor.get("default_disabled", False), - onewirehub.owproxy, + device_id=device_id, + device_name=device_names.get(device_id, device_id), + device_info=device_info, + entity_path=entity_path, + entity_specs=entity_specs, + owproxy=onewirehub.owproxy, ) ) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index f1b588690ae..da1ed01a980 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -157,30 +157,29 @@ def get_entities(onewirehub: OneWireHub): for device in onewirehub.devices: family = device["family"] device_type = device["type"] - sensor_id = os.path.split(os.path.split(device["path"])[0])[1] + device_id = os.path.split(os.path.split(device["path"])[0])[1] if family not in DEVICE_SWITCHES: continue device_info = { - "identifiers": {(DOMAIN, sensor_id)}, + "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, - "name": sensor_id, + "name": device_id, } - for device_switch in DEVICE_SWITCHES[family]: - device_file = os.path.join( - os.path.split(device["path"])[0], device_switch["path"] + for entity_specs in DEVICE_SWITCHES[family]: + entity_path = os.path.join( + os.path.split(device["path"])[0], entity_specs["path"] ) entities.append( OneWireProxySwitch( - sensor_id, - device_file, - device_switch["type"], - device_switch["name"], - device_info, - device_switch.get("default_disabled", False), - onewirehub.owproxy, + device_id=device_id, + device_name=device_id, + device_info=device_info, + entity_path=entity_path, + entity_specs=entity_specs, + owproxy=onewirehub.owproxy, ) ) From ff4897a09e3dbda8d168ad83c18e49469f873f20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Dec 2020 16:21:00 +0100 Subject: [PATCH 350/430] Fix using execute on the notify_leaving_zone (#43858) --- .../blueprints/notify_leaving_zone.yaml | 4 +++- tests/components/automation/test_blueprint.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 5b64a090a07..9b79396f066 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -27,6 +27,8 @@ trigger: variables: zone_entity: !input zone_entity zone_state: "{{ states[zone_entity].name }}" + person_entity: !input person_entity + person_name: "{{ states[person_entity].name }}" condition: condition: template @@ -36,4 +38,4 @@ action: domain: mobile_app type: notify device_id: !input notify_device - message: "{{ trigger.to_state.name }} has left {{ zone_state }}" + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 568087840c4..56062af17b7 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -115,6 +115,15 @@ async def test_notify_leaving_zone(hass): assert len(mock_call_action.mock_calls) == 2 + # Verify trigger works + await hass.services.async_call( + "automation", + "trigger", + {"entity_id": "automation.automation_0"}, + blocking=True, + ) + assert len(mock_call_action.mock_calls) == 3 + async def test_motion_light(hass): """Test motion light blueprint.""" @@ -192,3 +201,13 @@ async def test_motion_light(hass): assert len(turn_on_calls) == 3 assert len(turn_off_calls) == 1 + + # Verify trigger works + await hass.services.async_call( + "automation", + "trigger", + {"entity_id": "automation.automation_0"}, + ) + for _ in range(25): + await asyncio.sleep(0) + assert len(turn_on_calls) == 4 From ac2c01d20c5a75ae465186219ddf28fecffde125 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Dec 2020 16:21:27 +0100 Subject: [PATCH 351/430] deCONZ improve options updating entities (#42320) --- .../components/deconz/binary_sensor.py | 2 +- homeassistant/components/deconz/climate.py | 4 +- homeassistant/components/deconz/cover.py | 4 +- .../components/deconz/deconz_event.py | 6 +-- homeassistant/components/deconz/fan.py | 4 +- homeassistant/components/deconz/gateway.py | 43 ++++++++----------- homeassistant/components/deconz/light.py | 8 ++-- homeassistant/components/deconz/lock.py | 4 +- homeassistant/components/deconz/scene.py | 4 +- homeassistant/components/deconz/sensor.py | 2 +- homeassistant/components/deconz/services.py | 17 +++++--- homeassistant/components/deconz/switch.py | 4 +- 12 files changed, 49 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 184bce8defc..616206949ed 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors=gateway.api.sensors.values()): """Add binary sensor from deCONZ.""" entities = [] diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 0c1fe2da1e3..278d4144b6f 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_climate(sensors): + def async_add_climate(sensors=gateway.api.sensors.values()): """Add climate devices from deCONZ.""" entities = [] @@ -59,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_climate(gateway.api.sensors.values()) + async_add_climate() class DeconzThermostat(DeconzDevice, ClimateEntity): diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index c8d9a16d6c8..6e57d08302a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_cover(lights): + def async_add_cover(lights=gateway.api.lights.values()): """Add cover from deCONZ.""" entities = [] @@ -48,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_cover(gateway.api.lights.values()) + async_add_cover() class DeconzCover(DeconzDevice, CoverEntity): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 7ba372bf685..81d3aa94d31 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -16,7 +16,7 @@ async def async_setup_events(gateway) -> None: """Set up the deCONZ events.""" @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors=gateway.api.sensors.values()): """Create DeconzEvent.""" for sensor in sensors: @@ -38,9 +38,7 @@ async def async_setup_events(gateway) -> None: ) ) - async_add_sensor( - [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] - ) + async_add_sensor() @callback diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 69e77befb4f..d92addff5bd 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: gateway.entities[DOMAIN] = set() @callback - def async_add_fan(lights) -> None: + def async_add_fan(lights=gateway.api.lights.values()) -> None: """Add fan from deCONZ.""" entities = [] @@ -55,7 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: ) ) - async_add_fan(gateway.api.lights.values()) + async_add_fan() class DeconzFan(DeconzDevice, FanEntity): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8a9daded289..dc41bb778ec 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -55,9 +55,6 @@ class DeconzGateway: self.events = [] self.listeners = [] - self._current_option_allow_clip_sensor = self.option_allow_clip_sensor - self._current_option_allow_deconz_groups = self.option_allow_deconz_groups - @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" @@ -124,16 +121,20 @@ class DeconzGateway: async_dispatcher_send(self.hass, self.signal_reachable, True) @callback - def async_add_device_callback(self, device_type, device) -> None: + def async_add_device_callback(self, device_type, device=None) -> None: """Handle event of new device creation in deCONZ.""" if not self.option_allow_new_devices: return - if not isinstance(device, list): - device = [device] + args = [] + + if device is not None and not isinstance(device, list): + args.append([device]) async_dispatcher_send( - self.hass, self.async_signal_new_device(device_type), device + self.hass, + self.async_signal_new_device(device_type), + *args, # Don't send device if None, it would override default value in listeners ) async def async_update_device_registry(self) -> None: @@ -181,7 +182,7 @@ class DeconzGateway: ) ) - self.hass.async_create_task(async_setup_events(self)) + await async_setup_events(self) self.api.start() @@ -210,29 +211,21 @@ class DeconzGateway: """Manage entities affected by config entry options.""" deconz_ids = [] - if self._current_option_allow_clip_sensor != self.option_allow_clip_sensor: - self._current_option_allow_clip_sensor = self.option_allow_clip_sensor + if self.option_allow_clip_sensor: + self.async_add_device_callback(NEW_SENSOR) - sensors = [ - sensor + else: + deconz_ids += [ + sensor.deconz_id for sensor in self.api.sensors.values() if sensor.type.startswith("CLIP") ] - if self.option_allow_clip_sensor: - self.async_add_device_callback(NEW_SENSOR, sensors) - else: - deconz_ids += [sensor.deconz_id for sensor in sensors] + if self.option_allow_deconz_groups: + self.async_add_device_callback(NEW_GROUP) - if self._current_option_allow_deconz_groups != self.option_allow_deconz_groups: - self._current_option_allow_deconz_groups = self.option_allow_deconz_groups - - groups = list(self.api.groups.values()) - - if self.option_allow_deconz_groups: - self.async_add_device_callback(NEW_GROUP, groups) - else: - deconz_ids += [group.deconz_id for group in groups] + else: + deconz_ids += [group.deconz_id for group in self.api.groups.values()] entity_registry = await self.hass.helpers.entity_registry.async_get_registry() diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 01e36e2ccf7..be967a76fea 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): other_light_resource_types = CONTROLLER + COVER_TYPES + LOCK_TYPES + SWITCH_TYPES @callback - def async_add_light(lights): + def async_add_light(lights=gateway.api.lights.values()): """Add light from deCONZ.""" entities = [] @@ -66,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) @callback - def async_add_group(groups): + def async_add_group(groups=gateway.api.groups.values()): """Add group from deCONZ.""" if not gateway.option_allow_deconz_groups: return @@ -91,8 +91,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_light(gateway.api.lights.values()) - async_add_group(gateway.api.groups.values()) + async_add_light() + async_add_group() class DeconzBaseLight(DeconzDevice, LightEntity): diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index a5b53e86af5..4d428af3673 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_lock(lights): + def async_add_lock(lights=gateway.api.lights.values()): """Add lock from deCONZ.""" entities = [] @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_lock(gateway.api.lights.values()) + async_add_lock() class DeconzLock(DeconzDevice, LockEntity): diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 9ca7f39f034..4fbc1bfe453 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) @callback - def async_add_scene(scenes): + def async_add_scene(scenes=gateway.api.scenes.values()): """Add scene from deCONZ.""" entities = [DeconzScene(scene, gateway) for scene in scenes] @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_scene(gateway.api.scenes.values()) + async_add_scene() class DeconzScene(Scene): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 7b55dbbf823..9d71fd0a9f9 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): battery_handler = DeconzBatteryHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors=gateway.api.sensors.values()): """Add sensors from deCONZ. Create DeconzBattery if sensor has a battery attribute. diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 65b9c1aad8a..2c286fac0a1 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,4 +1,7 @@ """deCONZ services.""" + +import asyncio + from pydeconz.utils import normalize_bridge_id import voluptuous as vol @@ -143,10 +146,10 @@ async def async_refresh_devices_service(hass, data): await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP, list(gateway.api.groups.values())) - gateway.async_add_device_callback(NEW_LIGHT, list(gateway.api.lights.values())) - gateway.async_add_device_callback(NEW_SCENE, list(gateway.api.scenes.values())) - gateway.async_add_device_callback(NEW_SENSOR, list(gateway.api.sensors.values())) + gateway.async_add_device_callback(NEW_GROUP) + gateway.async_add_device_callback(NEW_LIGHT) + gateway.async_add_device_callback(NEW_SCENE) + gateway.async_add_device_callback(NEW_SENSOR) async def async_remove_orphaned_entries_service(hass, data): @@ -155,8 +158,10 @@ async def async_remove_orphaned_entries_service(hass, data): if CONF_BRIDGE_ID in data: gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry, entity_registry = await asyncio.gather( + hass.helpers.device_registry.async_get_registry(), + hass.helpers.entity_registry.async_get_registry(), + ) entity_entries = async_entries_for_config_entry( entity_registry, gateway.config_entry.entry_id diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 84bb0f84da1..f497e06c7af 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_switch(lights): + def async_add_switch(lights=gateway.api.lights.values()): """Add switch from deCONZ.""" entities = [] @@ -43,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_switch(gateway.api.lights.values()) + async_add_switch() class DeconzPowerPlug(DeconzDevice, SwitchEntity): From 58648019c6df4fb3f419ae4144e2a041ddccf455 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Dec 2020 16:55:33 +0100 Subject: [PATCH 352/430] Deprecate the use of keyring and credstash (#43854) --- homeassistant/util/yaml/loader.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 18beb2d4b14..746806f527d 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -32,6 +32,9 @@ DICT_T = TypeVar("DICT_T", bound=Dict) # pylint: disable=invalid-name _LOGGER = logging.getLogger(__name__) __SECRET_CACHE: Dict[str, JSON_TYPE] = {} +CREDSTASH_WARN = False +KEYRING_WARN = False + def clear_secret_cache() -> None: """Clear the secret cache. @@ -295,6 +298,14 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: # do some keyring stuff pwd = keyring.get_password(_SECRET_NAMESPACE, node.value) if pwd: + global KEYRING_WARN # pylint: disable=global-statement + + if not KEYRING_WARN: + KEYRING_WARN = True + _LOGGER.warning( + "Keyring is deprecated and will be removed in March 2021." + ) + _LOGGER.debug("Secret %s retrieved from keyring", node.value) return pwd @@ -305,6 +316,13 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: try: pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE) if pwd: + global CREDSTASH_WARN # pylint: disable=global-statement + + if not CREDSTASH_WARN: + CREDSTASH_WARN = True + _LOGGER.warning( + "Credstash is deprecated and will be removed in March 2021." + ) _LOGGER.debug("Secret %s retrieved from credstash", node.value) return pwd except credstash.ItemNotFound: From edb246d696e70064c27e00f42d7dda5791395613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 2 Dec 2020 17:01:55 +0100 Subject: [PATCH 353/430] Refactor Apple TV integration (#31952) Co-authored-by: Franck Nijhof --- .coveragerc | 4 +- CODEOWNERS | 1 + homeassistant/components/apple_tv/__init__.py | 556 ++++++++++------- .../components/apple_tv/config_flow.py | 408 ++++++++++++ homeassistant/components/apple_tv/const.py | 11 + .../components/apple_tv/manifest.json | 14 +- .../components/apple_tv/media_player.py | 173 ++---- homeassistant/components/apple_tv/remote.py | 72 +-- .../components/apple_tv/services.yaml | 8 - .../components/apple_tv/strings.json | 64 ++ .../components/apple_tv/translations/en.json | 64 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 10 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/apple_tv/__init__.py | 5 + tests/components/apple_tv/common.py | 49 ++ tests/components/apple_tv/conftest.py | 131 ++++ tests/components/apple_tv/test_config_flow.py | 582 ++++++++++++++++++ 19 files changed, 1758 insertions(+), 400 deletions(-) create mode 100644 homeassistant/components/apple_tv/config_flow.py create mode 100644 homeassistant/components/apple_tv/const.py delete mode 100644 homeassistant/components/apple_tv/services.yaml create mode 100644 homeassistant/components/apple_tv/strings.json create mode 100644 homeassistant/components/apple_tv/translations/en.json create mode 100644 tests/components/apple_tv/__init__.py create mode 100644 tests/components/apple_tv/common.py create mode 100644 tests/components/apple_tv/conftest.py create mode 100644 tests/components/apple_tv/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 40ae410eadc..960850fa99e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -48,7 +48,9 @@ omit = homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apcupsd/* - homeassistant/components/apple_tv/* + homeassistant/components/apple_tv/__init__.py + homeassistant/components/apple_tv/media_player.py + homeassistant/components/apple_tv/remote.py homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py homeassistant/components/arcam_fmj/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 2ca8a0a0f85..fe3af4c1ee6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -37,6 +37,7 @@ homeassistant/components/amcrest/* @pnbruckner homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core +homeassistant/components/apple_tv/* @postlund homeassistant/components/apprise/* @caronc homeassistant/components/aprs/* @PhilRW homeassistant/components/arcam_fmj/* @elupus diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 80c09606dbf..14170fdd8cd 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -1,273 +1,363 @@ -"""Support for Apple TV.""" +"""The Apple TV integration.""" import asyncio import logging -from typing import Sequence, TypeVar, Union +from random import randrange -from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs -from pyatv.exceptions import DeviceAuthenticationError -import voluptuous as vol +from pyatv import connect, exceptions, scan +from pyatv.const import Protocol -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.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.const import ( + CONF_ADDRESS, + CONF_NAME, + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback 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 .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN _LOGGER = logging.getLogger(__name__) -DOMAIN = "apple_tv" - -SERVICE_SCAN = "apple_tv_scan" -SERVICE_AUTHENTICATE = "apple_tv_authenticate" - -ATTR_ATV = "atv" -ATTR_POWER = "power" - -CONF_LOGIN_ID = "login_id" -CONF_START_OFF = "start_off" -CONF_CREDENTIALS = "credentials" - DEFAULT_NAME = "Apple TV" -DATA_APPLE_TV = "data_apple_tv" -DATA_ENTITIES = "data_apple_tv_entities" +BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes -KEY_CONFIG = "apple_tv_configuring" +NOTIFICATION_TITLE = "Apple TV Notification" +NOTIFICATION_ID = "apple_tv_notification" -NOTIFICATION_AUTH_ID = "apple_tv_auth_notification" -NOTIFICATION_AUTH_TITLE = "Apple TV Authentication" -NOTIFICATION_SCAN_ID = "apple_tv_scan_notification" -NOTIFICATION_SCAN_TITLE = "Apple TV Scan" +SOURCE_REAUTH = "reauth" -T = TypeVar("T") +SIGNAL_CONNECTED = "apple_tv_connected" +SIGNAL_DISCONNECTED = "apple_tv_disconnected" - -# This version of ensure_list interprets an empty dict as no value -def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]: - """Wrap value in list if it is not one.""" - if value is None or (isinstance(value, dict) and not value): - return [] - return value if isinstance(value, list) else [value] - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_LOGIN_ID): cv.string, - vol.Optional(CONF_CREDENTIALS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_START_OFF, default=False): cv.boolean, - } - ) - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) - -# Currently no attributes but it might change later -APPLE_TV_SCAN_SCHEMA = vol.Schema({}) - -APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) - - -def request_configuration(hass, config, atv, credentials): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - async def configuration_callback(callback_data): - """Handle the submitted configuration.""" - - pin = callback_data.get("pin") - - try: - await atv.airplay.finish_authentication(pin) - hass.components.persistent_notification.async_create( - f"Authentication succeeded!

" - f"Add the following to credentials: " - f"in your apple_tv configuration:

{credentials}", - title=NOTIFICATION_AUTH_TITLE, - notification_id=NOTIFICATION_AUTH_ID, - ) - except DeviceAuthenticationError as ex: - hass.components.persistent_notification.async_create( - f"Authentication failed! Did you enter correct PIN?

Details: {ex}", - title=NOTIFICATION_AUTH_TITLE, - notification_id=NOTIFICATION_AUTH_ID, - ) - - hass.async_add_job(configurator.request_done, instance) - - instance = configurator.request_config( - "Apple TV Authentication", - configuration_callback, - description="Please enter PIN code shown on screen.", - submit_caption="Confirm", - fields=[{"id": "pin", "name": "PIN Code", "type": "password"}], - ) - - -async def scan_apple_tvs(hass): - """Scan for devices and present a notification of the ones found.""" - - atvs = await scan_for_apple_tvs(hass.loop, timeout=3) - - devices = [] - for atv in atvs: - login_id = atv.login_id - if login_id is None: - login_id = "Home Sharing disabled" - devices.append( - f"Name: {atv.name}
Host: {atv.address}
Login ID: {login_id}" - ) - - if not devices: - devices = ["No device(s) found"] - - found_devices = "

".join(devices) - - hass.components.persistent_notification.async_create( - f"The following devices were found:

{found_devices}", - title=NOTIFICATION_SCAN_TITLE, - notification_id=NOTIFICATION_SCAN_ID, - ) +PLATFORMS = [MP_DOMAIN, REMOTE_DOMAIN] async def async_setup(hass, config): - """Set up the Apple TV component.""" - if DATA_APPLE_TV not in hass.data: - hass.data[DATA_APPLE_TV] = {} + """Set up the Apple TV integration.""" + return True - async def async_service_handler(service): - """Handle service calls.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - if service.service == SERVICE_SCAN: - hass.async_add_job(scan_apple_tvs, hass) - return +async def async_setup_entry(hass, entry): + """Set up a config entry for Apple TV.""" + manager = AppleTVManager(hass, entry) + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager - if entity_ids: - devices = [ - device - for device in hass.data[DATA_ENTITIES] - if device.entity_id in entity_ids + async def on_hass_stop(event): + """Stop push updates when hass stops.""" + await manager.disconnect() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + + async def setup_platforms(): + """Set up platforms and initiate connection.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS ] - else: - devices = hass.data[DATA_ENTITIES] - - for device in devices: - if service.service != SERVICE_AUTHENTICATE: - continue - - atv = device.atv - credentials = await atv.airplay.generate_credentials() - await atv.airplay.load_credentials(credentials) - _LOGGER.debug("Generated new credentials: %s", credentials) - await atv.airplay.start_authentication() - hass.async_add_job(request_configuration, hass, config, atv, credentials) - - async def atv_discovered(service, info): - """Set up an Apple TV that was auto discovered.""" - await _setup_atv( - hass, - config, - { - CONF_NAME: info["name"], - CONF_HOST: info["host"], - CONF_LOGIN_ID: info["properties"]["hG"], - CONF_START_OFF: False, - }, ) + await manager.init() - discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) - - tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])] - if tasks: - await asyncio.wait(tasks) - - hass.services.async_register( - DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA - ) - - hass.services.async_register( - DOMAIN, - SERVICE_AUTHENTICATE, - async_service_handler, - schema=APPLE_TV_AUTHENTICATE_SCHEMA, - ) + hass.async_create_task(setup_platforms()) return True -async def _setup_atv(hass, hass_config, atv_config): - """Set up an Apple TV.""" - - name = atv_config.get(CONF_NAME) - host = atv_config.get(CONF_HOST) - login_id = atv_config.get(CONF_LOGIN_ID) - start_off = atv_config.get(CONF_START_OFF) - credentials = atv_config.get(CONF_CREDENTIALS) - - if host in hass.data[DATA_APPLE_TV]: - return - - details = AppleTVDevice(name, host, login_id) - session = async_get_clientsession(hass) - atv = connect_to_apple_tv(details, hass.loop, session=session) - if credentials: - await atv.airplay.load_credentials(credentials) - - power = AppleTVPowerManager(hass, atv, start_off) - hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power} - - hass.async_create_task( - discovery.async_load_platform( - hass, "media_player", DOMAIN, atv_config, hass_config +async def async_unload_entry(hass, entry): + """Unload an Apple TV config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] ) ) + if unload_ok: + manager = hass.data[DOMAIN].pop(entry.unique_id) + await manager.disconnect() - hass.async_create_task( - discovery.async_load_platform(hass, "remote", DOMAIN, atv_config, hass_config) - ) + return unload_ok -class AppleTVPowerManager: - """Manager for global power management of an Apple TV. +class AppleTVEntity(Entity): + """Device that sends commands to an Apple TV.""" - An instance is used per device to share the same power state between - several platforms. - """ + def __init__(self, name, identifier, manager): + """Initialize device.""" + self.atv = None + self.manager = manager + self._name = name + self._identifier = identifier - def __init__(self, hass, atv, is_off): - """Initialize power manager.""" - self.hass = hass - self.atv = atv - self.listeners = [] - self._is_on = not is_off + async def async_added_to_hass(self): + """Handle when an entity is about to be added to Home Assistant.""" - def init(self): - """Initialize power management.""" - if self._is_on: - self.atv.push_updater.start() + @callback + def _async_connected(atv): + """Handle that a connection was made to a device.""" + self.atv = atv + self.async_device_connected(atv) + self.async_write_ha_state() + + @callback + def _async_disconnected(): + """Handle that a connection to a device was lost.""" + self.async_device_disconnected() + self.atv = None + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"{SIGNAL_CONNECTED}_{self._identifier}", _async_connected + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_DISCONNECTED}_{self._identifier}", + _async_disconnected, + ) + ) + + def async_device_connected(self, atv): + """Handle when connection is made to device.""" + + def async_device_disconnected(self): + """Handle when connection was lost to device.""" @property - def turned_on(self): - """Return true if device is on or off.""" - return self._is_on + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._identifier)}, + "manufacturer": "Apple", + "name": self.name, + } - def set_power_on(self, value): - """Change if a device is on or off.""" - if value != self._is_on: - self._is_on = value - if not self._is_on: + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return a unique ID.""" + return self._identifier + + @property + def should_poll(self): + """No polling needed for Apple TV.""" + return False + + +class AppleTVManager: + """Connection and power manager for an Apple TV. + + An instance is used per device to share the same power state between + several platforms. It also manages scanning and connection establishment + in case of problems. + """ + + def __init__(self, hass, config_entry): + """Initialize power manager.""" + self.config_entry = config_entry + self.hass = hass + self.atv = None + self._is_on = not config_entry.options.get(CONF_START_OFF, False) + self._connection_attempts = 0 + self._connection_was_lost = False + self._task = None + + async def init(self): + """Initialize power management.""" + if self._is_on: + await self.connect() + + def connection_lost(self, _): + """Device was unexpectedly disconnected. + + This is a callback function from pyatv.interface.DeviceListener. + """ + _LOGGER.warning('Connection lost to Apple TV "%s"', self.atv.name) + if self.atv: + self.atv.close() + self.atv = None + self._connection_was_lost = True + self._dispatch_send(SIGNAL_DISCONNECTED) + self._start_connect_loop() + + def connection_closed(self): + """Device connection was (intentionally) closed. + + This is a callback function from pyatv.interface.DeviceListener. + """ + if self.atv: + self.atv.close() + self.atv = None + self._dispatch_send(SIGNAL_DISCONNECTED) + self._start_connect_loop() + + async def connect(self): + """Connect to device.""" + self._is_on = True + self._start_connect_loop() + + async def disconnect(self): + """Disconnect from device.""" + _LOGGER.debug("Disconnecting from device") + self._is_on = False + try: + if self.atv: + self.atv.push_updater.listener = None self.atv.push_updater.stop() - else: - self.atv.push_updater.start() + self.atv.close() + self.atv = None + if self._task: + self._task.cancel() + self._task = None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An error occurred while disconnecting") - for listener in self.listeners: - self.hass.async_create_task(listener.async_update_ha_state()) + def _start_connect_loop(self): + """Start background connect loop to device.""" + if not self._task and self.atv is None and self._is_on: + self._task = asyncio.create_task(self._connect_loop()) + else: + _LOGGER.debug( + "Not starting connect loop (%s, %s)", self.atv is None, self._is_on + ) + + async def _connect_loop(self): + """Connect loop background task function.""" + _LOGGER.debug("Starting connect loop") + + # Try to find device and connect as long as the user has said that + # we are allowed to connect and we are not already connected. + while self._is_on and self.atv is None: + try: + conf = await self._scan() + if conf: + await self._connect(conf) + except exceptions.AuthenticationError: + self._auth_problem() + break + except asyncio.CancelledError: + pass + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to connect") + self.atv = None + + if self.atv is None: + self._connection_attempts += 1 + backoff = min( + randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT + ) + + _LOGGER.debug("Reconnecting in %d seconds", backoff) + await asyncio.sleep(backoff) + + _LOGGER.debug("Connect loop ended") + self._task = None + + def _auth_problem(self): + """Problem to authenticate occurred that needs intervention.""" + _LOGGER.debug("Authentication error, reconfigure integration") + + name = self.config_entry.data.get(CONF_NAME) + identifier = self.config_entry.unique_id + + self.hass.components.persistent_notification.create( + "An irrecoverable connection problem occurred when connecting to " + f"`f{name}`. Please go to the Integrations page and reconfigure it", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) + + # Add to event queue as this function is called from a task being + # cancelled from disconnect + asyncio.create_task(self.disconnect()) + + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_NAME: name, CONF_IDENTIFIER: identifier}, + ) + ) + + async def _scan(self): + """Try to find device by scanning for it.""" + identifier = self.config_entry.unique_id + address = self.config_entry.data[CONF_ADDRESS] + protocol = Protocol(self.config_entry.data[CONF_PROTOCOL]) + + _LOGGER.debug("Discovering device %s", identifier) + atvs = await scan( + self.hass.loop, identifier=identifier, protocol=protocol, hosts=[address] + ) + if atvs: + return atvs[0] + + _LOGGER.debug( + "Failed to find device %s with address %s, trying to scan", + identifier, + address, + ) + + atvs = await scan(self.hass.loop, identifier=identifier, protocol=protocol) + if atvs: + return atvs[0] + + _LOGGER.debug("Failed to find device %s, trying later", identifier) + + return None + + async def _connect(self, conf): + """Connect to device.""" + credentials = self.config_entry.data[CONF_CREDENTIALS] + session = async_get_clientsession(self.hass) + + for protocol, creds in credentials.items(): + conf.set_credentials(Protocol(int(protocol)), creds) + + _LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME]) + self.atv = await connect(conf, self.hass.loop, session=session) + self.atv.listener = self + + self._dispatch_send(SIGNAL_CONNECTED, self.atv) + self._address_updated(str(conf.address)) + + self._connection_attempts = 0 + if self._connection_was_lost: + _LOGGER.info( + 'Connection was re-established to Apple TV "%s"', self.atv.service.name + ) + self._connection_was_lost = False + + @property + def is_connecting(self): + """Return true if connection is in progress.""" + return self._task is not None + + def _address_updated(self, address): + """Update cached address in config entry.""" + _LOGGER.debug("Changing address to %s", address) + self.hass.config_entries.async_update_entry( + self.config_entry, data={**self.config_entry.data, CONF_ADDRESS: address} + ) + + def _dispatch_send(self, signal, *args): + """Dispatch a signal to all entities managed by this manager.""" + async_dispatcher_send( + self.hass, f"{signal}_{self.config_entry.unique_id}", *args + ) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py new file mode 100644 index 00000000000..9c2f25b6d53 --- /dev/null +++ b/homeassistant/components/apple_tv/config_flow.py @@ -0,0 +1,408 @@ +"""Config flow for Apple TV integration.""" +from ipaddress import ip_address +import logging +from random import randrange + +from pyatv import exceptions, pair, scan +from pyatv.const import Protocol +from pyatv.convert import protocol_str +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_ADDRESS, + CONF_NAME, + CONF_PIN, + CONF_PROTOCOL, + CONF_TYPE, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DEVICE_INPUT = "device_input" + +INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int}) + +DEFAULT_START_OFF = False +PROTOCOL_PRIORITY = [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay] + + +async def device_scan(identifier, loop, cache=None): + """Scan for a specific device using identifier as filter.""" + + def _filter_device(dev): + if identifier is None: + return True + if identifier == str(dev.address): + return True + if identifier == dev.name: + return True + return any([service.identifier == identifier for service in dev.services]) + + def _host_filter(): + try: + return [ip_address(identifier)] + except ValueError: + return None + + if cache: + matches = [atv for atv in cache if _filter_device(atv)] + if matches: + return cache, matches[0] + + for hosts in [_host_filter(), None]: + scan_result = await scan(loop, timeout=3, hosts=hosts) + matches = [atv for atv in scan_result if _filter_device(atv)] + + if matches: + return scan_result, matches[0] + + return scan_result, None + + +def is_valid_credentials(credentials): + """Verify that credentials are valid for establishing a connection.""" + return ( + credentials.get(Protocol.MRP.value) is not None + or credentials.get(Protocol.DMAP.value) is not None + ) + + +class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Apple TV.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow for this handler.""" + return AppleTVOptionsFlow(config_entry) + + def __init__(self): + """Initialize a new AppleTVConfigFlow.""" + self.target_device = None + self.scan_result = None + self.atv = None + self.protocol = None + self.pairing = None + self.credentials = {} # Protocol -> credentials + + async def async_step_reauth(self, info): + """Handle initial step when updating invalid credentials.""" + await self.async_set_unique_id(info[CONF_IDENTIFIER]) + self.target_device = info[CONF_IDENTIFIER] + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {"name": info[CONF_NAME]} + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["identifier"] = self.unique_id + return await self.async_step_reconfigure() + + async def async_step_reconfigure(self, user_input=None): + """Inform user that reconfiguration is about to start.""" + if user_input is not None: + return await self.async_find_device_wrapper( + self.async_begin_pairing, allow_exist=True + ) + + return self.async_show_form(step_id="reconfigure") + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + # Be helpful to the user and look for devices + if self.scan_result is None: + self.scan_result, _ = await device_scan(None, self.hass.loop) + + errors = {} + default_suggestion = self._prefill_identifier() + if user_input is not None: + self.target_device = user_input[DEVICE_INPUT] + try: + await self.async_find_device() + except DeviceNotFound: + errors["base"] = "no_devices_found" + except DeviceAlreadyConfigured: + errors["base"] = "already_configured" + except exceptions.NoServiceError: + errors["base"] = "no_usable_service" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + self.atv.identifier, raise_on_progress=False + ) + return await self.async_step_confirm() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(DEVICE_INPUT, default=default_suggestion): str} + ), + errors=errors, + description_placeholders={"devices": self._devices_str()}, + ) + + async def async_step_zeroconf(self, discovery_info): + """Handle device found via zeroconf.""" + service_type = discovery_info[CONF_TYPE] + properties = discovery_info["properties"] + + if service_type == "_mediaremotetv._tcp.local.": + identifier = properties["UniqueIdentifier"] + name = properties["Name"] + elif service_type == "_touch-able._tcp.local.": + identifier = discovery_info["name"].split(".")[0] + name = properties["CtlN"] + else: + return self.async_abort(reason="unknown") + + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["identifier"] = self.unique_id + self.context["title_placeholders"] = {"name": name} + self.target_device = identifier + return await self.async_find_device_wrapper(self.async_step_confirm) + + async def async_find_device_wrapper(self, next_func, allow_exist=False): + """Find a specific device and call another function when done. + + This function will do error handling and bail out when an error + occurs. + """ + try: + await self.async_find_device(allow_exist) + except DeviceNotFound: + return self.async_abort(reason="no_devices_found") + except DeviceAlreadyConfigured: + return self.async_abort(reason="already_configured") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + return await next_func() + + async def async_find_device(self, allow_exist=False): + """Scan for the selected device to discover services.""" + self.scan_result, self.atv = await device_scan( + self.target_device, self.hass.loop, cache=self.scan_result + ) + if not self.atv: + raise DeviceNotFound() + + self.protocol = self.atv.main_service().protocol + + if not allow_exist: + for identifier in self.atv.all_identifiers: + if identifier in self._async_current_ids(): + raise DeviceAlreadyConfigured() + + # If credentials were found, save them + for service in self.atv.services: + if service.credentials: + self.credentials[service.protocol.value] = service.credentials + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return await self.async_begin_pairing() + return self.async_show_form( + step_id="confirm", description_placeholders={"name": self.atv.name} + ) + + async def async_begin_pairing(self): + """Start pairing process for the next available protocol.""" + self.protocol = self._next_protocol_to_pair() + + # Dispose previous pairing sessions + if self.pairing is not None: + await self.pairing.close() + self.pairing = None + + # Any more protocols to pair? Else bail out here + if not self.protocol: + await self.async_set_unique_id(self.atv.main_service().identifier) + return self._async_get_entry( + self.atv.main_service().protocol, + self.atv.name, + self.credentials, + self.atv.address, + ) + + # Initiate the pairing process + abort_reason = None + session = async_get_clientsession(self.hass) + self.pairing = await pair( + self.atv, self.protocol, self.hass.loop, session=session + ) + try: + await self.pairing.begin() + except exceptions.ConnectionFailedError: + return await self.async_step_service_problem() + except exceptions.BackOffError: + abort_reason = "backoff" + except exceptions.PairingError: + _LOGGER.exception("Authentication problem") + abort_reason = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + abort_reason = "unknown" + + if abort_reason: + if self.pairing: + await self.pairing.close() + return self.async_abort(reason=abort_reason) + + # Choose step depending on if PIN is required from user or not + if self.pairing.device_provides_pin: + return await self.async_step_pair_with_pin() + + return await self.async_step_pair_no_pin() + + async def async_step_pair_with_pin(self, user_input=None): + """Handle pairing step where a PIN is required from the user.""" + errors = {} + if user_input is not None: + try: + self.pairing.pin(user_input[CONF_PIN]) + await self.pairing.finish() + self.credentials[self.protocol.value] = self.pairing.service.credentials + return await self.async_begin_pairing() + except exceptions.PairingError: + _LOGGER.exception("Authentication problem") + errors["base"] = "invalid_auth" + except AbortFlow: + raise + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="pair_with_pin", + data_schema=INPUT_PIN_SCHEMA, + errors=errors, + description_placeholders={"protocol": protocol_str(self.protocol)}, + ) + + async def async_step_pair_no_pin(self, user_input=None): + """Handle step where user has to enter a PIN on the device.""" + if user_input is not None: + await self.pairing.finish() + if self.pairing.has_paired: + self.credentials[self.protocol.value] = self.pairing.service.credentials + return await self.async_begin_pairing() + + await self.pairing.close() + return self.async_abort(reason="device_did_not_pair") + + pin = randrange(1000, stop=10000) + self.pairing.pin(pin) + return self.async_show_form( + step_id="pair_no_pin", + description_placeholders={ + "protocol": protocol_str(self.protocol), + "pin": pin, + }, + ) + + async def async_step_service_problem(self, user_input=None): + """Inform user that a service will not be added.""" + if user_input is not None: + self.credentials[self.protocol.value] = None + return await self.async_begin_pairing() + + return self.async_show_form( + step_id="service_problem", + description_placeholders={"protocol": protocol_str(self.protocol)}, + ) + + def _async_get_entry(self, protocol, name, credentials, address): + if not is_valid_credentials(credentials): + return self.async_abort(reason="invalid_config") + + data = { + CONF_PROTOCOL: protocol.value, + CONF_NAME: name, + CONF_CREDENTIALS: credentials, + CONF_ADDRESS: str(address), + } + + self._abort_if_unique_id_configured(reload_on_update=False, updates=data) + + return self.async_create_entry(title=name, data=data) + + def _next_protocol_to_pair(self): + def _needs_pairing(protocol): + if self.atv.get_service(protocol) is None: + return False + return protocol.value not in self.credentials + + for protocol in PROTOCOL_PRIORITY: + if _needs_pairing(protocol): + return protocol + return None + + def _devices_str(self): + return ", ".join( + [ + f"`{atv.name} ({atv.address})`" + for atv in self.scan_result + if atv.identifier not in self._async_current_ids() + ] + ) + + def _prefill_identifier(self): + # Return identifier (address) of one device that has not been paired with + for atv in self.scan_result: + if atv.identifier not in self._async_current_ids(): + return str(atv.address) + return "" + + +class AppleTVOptionsFlow(config_entries.OptionsFlow): + """Handle Apple TV options.""" + + def __init__(self, config_entry): + """Initialize Apple TV options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Apple TV options.""" + if user_input is not None: + self.options[CONF_START_OFF] = user_input[CONF_START_OFF] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_START_OFF, + default=self.config_entry.options.get( + CONF_START_OFF, DEFAULT_START_OFF + ), + ): bool, + } + ), + ) + + +class DeviceNotFound(HomeAssistantError): + """Error to indicate device could not be found.""" + + +class DeviceAlreadyConfigured(HomeAssistantError): + """Error to indicate device is already configured.""" diff --git a/homeassistant/components/apple_tv/const.py b/homeassistant/components/apple_tv/const.py new file mode 100644 index 00000000000..ac04cc1b937 --- /dev/null +++ b/homeassistant/components/apple_tv/const.py @@ -0,0 +1,11 @@ +"""Constants for the Apple TV integration.""" + +DOMAIN = "apple_tv" + +CONF_IDENTIFIER = "identifier" +CONF_CREDENTIALS = "credentials" +CONF_CREDENTIALS_MRP = "mrp" +CONF_CREDENTIALS_DMAP = "dmap" +CONF_CREDENTIALS_AIRPLAY = "airplay" + +CONF_START_OFF = "start_off" diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 8ca42beab61..ccd5da49547 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -1,9 +1,17 @@ { "domain": "apple_tv", "name": "Apple TV", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.3.13"], - "dependencies": ["configurator"], + "requirements": [ + "pyatv==0.7.3" + ], + "zeroconf": [ + "_mediaremotetv._tcp.local.", + "_touch-able._tcp.local." + ], "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [ + "@postlund" + ] } diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 72e7d88b364..b7486af50e9 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,7 +1,7 @@ """Support for Apple TV media player.""" import logging -import pyatv.const as atv_const +from pyatv.const import DeviceState, MediaType from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -19,9 +19,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, ) from homeassistant.const import ( - CONF_HOST, CONF_NAME, - EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, @@ -31,10 +29,13 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.util.dt as dt_util -from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES +from . import AppleTVEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + SUPPORT_APPLE_TV = ( SUPPORT_TURN_ON | SUPPORT_TURN_OFF @@ -48,108 +49,61 @@ SUPPORT_APPLE_TV = ( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Apple TV platform.""" - if not discovery_info: - return +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Apple TV media player based on a config entry.""" + name = config_entry.data[CONF_NAME] + manager = hass.data[DOMAIN][config_entry.unique_id] + async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)]) - # Manage entity cache for service handler - if DATA_ENTITIES not in hass.data: - hass.data[DATA_ENTITIES] = [] - name = discovery_info[CONF_NAME] - host = discovery_info[CONF_HOST] - atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV] - power = hass.data[DATA_APPLE_TV][host][ATTR_POWER] - entity = AppleTvDevice(atv, name, power) +class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): + """Representation of an Apple TV media player.""" + + def __init__(self, name, identifier, manager, **kwargs): + """Initialize the Apple TV media player.""" + super().__init__(name, identifier, manager, **kwargs) + self._playing = None @callback - def on_hass_stop(event): - """Stop push updates when hass stops.""" - atv.push_updater.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) - - if entity not in hass.data[DATA_ENTITIES]: - hass.data[DATA_ENTITIES].append(entity) - - async_add_entities([entity]) - - -class AppleTvDevice(MediaPlayerEntity): - """Representation of an Apple TV device.""" - - def __init__(self, atv, name, power): - """Initialize the Apple TV device.""" - self.atv = atv - self._name = name - self._playing = None - self._power = power - self._power.listeners.append(self) + def async_device_connected(self, atv): + """Handle when connection is made to device.""" self.atv.push_updater.listener = self + self.atv.push_updater.start() - async def async_added_to_hass(self): - """Handle when an entity is about to be added to Home Assistant.""" - self._power.init() - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self.atv.metadata.device_id - - @property - def should_poll(self): - """No polling needed.""" - return False + @callback + def async_device_disconnected(self): + """Handle when connection was lost to device.""" + self.atv.push_updater.stop() + self.atv.push_updater.listener = None @property def state(self): """Return the state of the device.""" - if not self._power.turned_on: + if self.manager.is_connecting: + return None + if self.atv is None: return STATE_OFF - if self._playing: - - state = self._playing.play_state - if state in ( - atv_const.PLAY_STATE_IDLE, - atv_const.PLAY_STATE_NO_MEDIA, - atv_const.PLAY_STATE_LOADING, - ): + state = self._playing.device_state + if state in (DeviceState.Idle, DeviceState.Loading): return STATE_IDLE - if state == atv_const.PLAY_STATE_PLAYING: + if state == DeviceState.Playing: return STATE_PLAYING - if state in ( - 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 + if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped): return STATE_PAUSED return STATE_STANDBY # Bad or unknown state? + return None @callback - def playstatus_update(self, updater, playing): + def playstatus_update(self, _, playing): """Print what is currently playing when it changes.""" self._playing = playing self.async_write_ha_state() @callback - def playstatus_error(self, updater, exception): + def playstatus_error(self, _, exception): """Inform about an error and restart push updates.""" _LOGGER.warning("A %s error occurred: %s", exception.__class__, exception) - - # This will wait 10 seconds before restarting push updates. If the - # connection continues to fail, it will flood the log (every 10 - # seconds) until it succeeds. A better approach should probably be - # implemented here later. - updater.start(initial_delay=10) self._playing = None self.async_write_ha_state() @@ -157,50 +111,53 @@ class AppleTvDevice(MediaPlayerEntity): def media_content_type(self): """Content type of current playing media.""" if self._playing: - - media_type = self._playing.media_type - if media_type == atv_const.MEDIA_TYPE_VIDEO: - return MEDIA_TYPE_VIDEO - if media_type == atv_const.MEDIA_TYPE_MUSIC: - return MEDIA_TYPE_MUSIC - if media_type == atv_const.MEDIA_TYPE_TV: - return MEDIA_TYPE_TVSHOW + return { + MediaType.Video: MEDIA_TYPE_VIDEO, + MediaType.Music: MEDIA_TYPE_MUSIC, + MediaType.TV: MEDIA_TYPE_TVSHOW, + }.get(self._playing.media_type) + return None @property def media_duration(self): """Duration of current playing media in seconds.""" if self._playing: return self._playing.total_time + return None @property def media_position(self): """Position of current playing media in seconds.""" if self._playing: return self._playing.position + return None @property def media_position_updated_at(self): """Last valid time of media position.""" - state = self.state - if state in (STATE_PLAYING, STATE_PAUSED): + if self.state in (STATE_PLAYING, STATE_PAUSED): return dt_util.utcnow() + return None async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - await self.atv.airplay.play_url(media_id) + await self.atv.stream.play_url(media_id) @property def media_image_hash(self): """Hash value for media image.""" state = self.state - if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return self._playing.hash + if self._playing and state not in [None, STATE_OFF, STATE_IDLE]: + return self.atv.metadata.artwork_id + return None async def async_get_media_image(self): """Fetch media image of current playing image.""" state = self.state if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return (await self.atv.metadata.artwork()), "image/png" + artwork = await self.atv.metadata.artwork() + if artwork: + return artwork.bytes, artwork.mimetype return None, None @@ -208,12 +165,8 @@ class AppleTvDevice(MediaPlayerEntity): def media_title(self): """Title of current playing media.""" if self._playing: - if self.state == STATE_IDLE: - return "Nothing playing" - title = self._playing.title - return title if title else "No title" - - return f"Establishing a connection to {self._name}..." + return self._playing.title + return None @property def supported_features(self): @@ -222,22 +175,22 @@ class AppleTvDevice(MediaPlayerEntity): async def async_turn_on(self): """Turn the media player on.""" - self._power.set_power_on(True) + await self.manager.connect() async def async_turn_off(self): """Turn the media player off.""" self._playing = None - self._power.set_power_on(False) + await self.manager.disconnect() async def async_media_play_pause(self): """Pause media on media player.""" - if not self._playing: - return - state = self.state - if state == STATE_PAUSED: - await self.atv.remote_control.play() - elif state == STATE_PLAYING: - await self.atv.remote_control.pause() + if self._playing: + state = self.state + if state == STATE_PAUSED: + await self.atv.remote_control.play() + elif state == STATE_PLAYING: + await self.atv.remote_control.pause() + return None async def async_media_play(self): """Play media.""" diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 4f935ba0ab8..a76c4c6a208 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -1,46 +1,32 @@ """Remote control support for Apple TV.""" -from homeassistant.components import remote -from homeassistant.const import CONF_HOST, CONF_NAME -from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV +import logging + +from homeassistant.components.remote import RemoteEntity +from homeassistant.const import CONF_NAME + +from . import AppleTVEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Apple TV remote platform.""" - if not discovery_info: - return - - name = discovery_info[CONF_NAME] - host = discovery_info[CONF_HOST] - atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV] - power = hass.data[DATA_APPLE_TV][host][ATTR_POWER] - async_add_entities([AppleTVRemote(atv, power, name)]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Apple TV remote based on a config entry.""" + name = config_entry.data[CONF_NAME] + manager = hass.data[DOMAIN][config_entry.unique_id] + async_add_entities([AppleTVRemote(name, config_entry.unique_id, manager)]) -class AppleTVRemote(remote.RemoteEntity): +class AppleTVRemote(AppleTVEntity, RemoteEntity): """Device that sends commands to an Apple TV.""" - def __init__(self, atv, power, name): - """Initialize device.""" - self._atv = atv - self._name = name - self._power = power - self._power.listeners.append(self) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._atv.metadata.device_id - @property def is_on(self): """Return true if device is on.""" - return self._power.turned_on + return self.atv is not None @property def should_poll(self): @@ -48,23 +34,21 @@ class AppleTVRemote(remote.RemoteEntity): return False async def async_turn_on(self, **kwargs): - """Turn the device on. - - This method is a coroutine. - """ - self._power.set_power_on(True) + """Turn the device on.""" + await self.manager.connect() async def async_turn_off(self, **kwargs): - """Turn the device off. - - This method is a coroutine. - """ - self._power.set_power_on(False) + """Turn the device off.""" + await self.manager.disconnect() async def async_send_command(self, command, **kwargs): """Send a command to one device.""" + if not self.is_on: + _LOGGER.error("Unable to send commands, not connected to %s", self._name) + return + for single_command in command: - if not hasattr(self._atv.remote_control, single_command): + if not hasattr(self.atv.remote_control, single_command): continue - await getattr(self._atv.remote_control, single_command)() + await getattr(self.atv.remote_control, single_command)() diff --git a/homeassistant/components/apple_tv/services.yaml b/homeassistant/components/apple_tv/services.yaml deleted file mode 100644 index af1e052fa33..00000000000 --- a/homeassistant/components/apple_tv/services.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apple_tv_authenticate: - description: Start AirPlay device authentication. - fields: - entity_id: - description: Name(s) of entities to authenticate with. - example: media_player.apple_tv -apple_tv_scan: - description: Scan for Apple TV devices. diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json new file mode 100644 index 00000000000..e990fa0de06 --- /dev/null +++ b/homeassistant/components/apple_tv/strings.json @@ -0,0 +1,64 @@ +{ + "title": "Apple TV", + "config": { + "flow_title": "Apple TV: {name}", + "step": { + "user": { + "title": "Setup a new Apple TV", + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", + "data": { + "device_input": "Device" + } + }, + "reconfigure": { + "title": "Device reconfiguration", + "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured." + }, + "pair_with_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + }, + "pair_no_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue." + }, + "service_problem": { + "title": "Failed to add service", + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." + }, + "confirm": { + "title": "Confirm adding Apple TV", + "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!" + } + }, + "error": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "init": { + "description": "Configure general device settings", + "data": { + "start_off": "Do not turn device on when starting Home Assistant" + } + } + } + } +} diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json new file mode 100644 index 00000000000..8507c84af3b --- /dev/null +++ b/homeassistant/components/apple_tv/translations/en.json @@ -0,0 +1,64 @@ +{ + "title": "Apple TV", + "config": { + "flow_title": "Apple TV: {name}", + "step": { + "user": { + "title": "Setup a new Apple TV", + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", + "data": { + "device_input": "Device" + } + }, + "reconfigure": { + "title": "Device reconfiguration", + "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured." + }, + "pair_with_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "data": { + "pin": "PIN Code" + } + }, + "pair_no_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue." + }, + "service_problem": { + "title": "Failed to add service", + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." + }, + "confirm": { + "title": "Confirm adding Apple TV", + "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!" + } + }, + "error": { + "no_devices_found": "No devices found on the network", + "already_configured": "Device is already configured", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", + "unknown": "Unexpected error", + "invalid_auth": "Invalid authentication" + }, + "abort": { + "no_devices_found": "No devices found on the network", + "already_configured_device": "Device is already configured", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "already_in_progress": "Configuration flow is already in progress", + "unknown": "Unexpected error" + } + }, + "options": { + "step": { + "init": { + "description": "Configure general device settings", + "data": { + "start_off": "Do not turn device on when starting Home Assistant" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d9559e9085f..a8e871aa02e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -18,6 +18,7 @@ FLOWS = [ "almond", "ambiclimate", "ambient_station", + "apple_tv", "arcam_fmj", "atag", "august", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 60dcf04d9ed..6efa44e304f 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -90,6 +90,11 @@ ZEROCONF = { "domain": "ipp" } ], + "_mediaremotetv._tcp.local.": [ + { + "domain": "apple_tv" + } + ], "_miio._udp.local.": [ { "domain": "xiaomi_aqara" @@ -129,6 +134,11 @@ ZEROCONF = { "name": "smappee2*" } ], + "_touch-able._tcp.local.": [ + { + "domain": "apple_tv" + } + ], "_viziocast._tcp.local.": [ { "domain": "vizio" diff --git a/requirements_all.txt b/requirements_all.txt index f475a25e19b..5a08853fb80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ pyatmo==4.2.1 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.3.13 +pyatv==0.7.3 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab1012beb1f..9c4efc55037 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,6 +648,9 @@ pyatag==0.3.4.4 # homeassistant.components.netatmo pyatmo==4.2.1 +# homeassistant.components.apple_tv +pyatv==0.7.3 + # homeassistant.components.blackbird pyblackbird==0.5 diff --git a/tests/components/apple_tv/__init__.py b/tests/components/apple_tv/__init__.py new file mode 100644 index 00000000000..118c3d6f735 --- /dev/null +++ b/tests/components/apple_tv/__init__.py @@ -0,0 +1,5 @@ +"""Tests for Apple TV.""" +import pytest + +# Make asserts in the common module display differences +pytest.register_assert_rewrite("tests.components.apple_tv.common") diff --git a/tests/components/apple_tv/common.py b/tests/components/apple_tv/common.py new file mode 100644 index 00000000000..6f13239edcb --- /dev/null +++ b/tests/components/apple_tv/common.py @@ -0,0 +1,49 @@ +"""Test code shared between test files.""" + +from pyatv import conf, interface +from pyatv.const import Protocol + + +class MockPairingHandler(interface.PairingHandler): + """Mock for PairingHandler in pyatv.""" + + def __init__(self, *args): + """Initialize a new MockPairingHandler.""" + super().__init__(*args) + self.pin_code = None + self.paired = False + self.always_fail = False + + def pin(self, pin): + """Pin code used for pairing.""" + self.pin_code = pin + self.paired = False + + @property + def device_provides_pin(self): + """Return True if remote device presents PIN code, else False.""" + return self.service.protocol in [Protocol.MRP, Protocol.AirPlay] + + @property + def has_paired(self): + """If a successful pairing has been performed. + + The value will be reset when stop() is called. + """ + return not self.always_fail and self.paired + + async def begin(self): + """Start pairing process.""" + + async def finish(self): + """Stop pairing process.""" + self.paired = True + self.service.credentials = self.service.protocol.name.lower() + "_creds" + + +def create_conf(name, address, *services): + """Create an Apple TV configuration.""" + atv = conf.AppleTV(name, address) + for service in services: + atv.add_service(service) + return atv diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py new file mode 100644 index 00000000000..50b57e073d9 --- /dev/null +++ b/tests/components/apple_tv/conftest.py @@ -0,0 +1,131 @@ +"""Fixtures for component.""" + +from pyatv import conf, net +import pytest + +from .common import MockPairingHandler, create_conf + +from tests.async_mock import patch + + +@pytest.fixture(autouse=True, name="mock_scan") +def mock_scan_fixture(): + """Mock pyatv.scan.""" + with patch("homeassistant.components.apple_tv.config_flow.scan") as mock_scan: + + async def _scan(loop, timeout=5, identifier=None, protocol=None, hosts=None): + if not mock_scan.hosts: + mock_scan.hosts = hosts + return mock_scan.result + + mock_scan.result = [] + mock_scan.hosts = None + mock_scan.side_effect = _scan + yield mock_scan + + +@pytest.fixture(name="dmap_pin") +def dmap_pin_fixture(): + """Mock pyatv.scan.""" + with patch("homeassistant.components.apple_tv.config_flow.randrange") as mock_pin: + mock_pin.side_effect = lambda start, stop: 1111 + yield mock_pin + + +@pytest.fixture +def pairing(): + """Mock pyatv.scan.""" + with patch("homeassistant.components.apple_tv.config_flow.pair") as mock_pair: + + async def _pair(config, protocol, loop, session=None, **kwargs): + handler = MockPairingHandler( + await net.create_session(session), config.get_service(protocol) + ) + handler.always_fail = mock_pair.always_fail + return handler + + mock_pair.always_fail = False + mock_pair.side_effect = _pair + yield mock_pair + + +@pytest.fixture +def pairing_mock(): + """Mock pyatv.scan.""" + with patch("homeassistant.components.apple_tv.config_flow.pair") as mock_pair: + + async def _pair(config, protocol, loop, session=None, **kwargs): + return mock_pair + + async def _begin(): + pass + + async def _close(): + pass + + mock_pair.close.side_effect = _close + mock_pair.begin.side_effect = _begin + mock_pair.pin = lambda pin: None + mock_pair.side_effect = _pair + yield mock_pair + + +@pytest.fixture +def full_device(mock_scan, dmap_pin): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf( + "127.0.0.1", + "MRP Device", + conf.MrpService("mrpid", 5555), + conf.DmapService("dmapid", None, port=6666), + conf.AirPlayService("airplayid", port=7777), + ) + ) + yield mock_scan + + +@pytest.fixture +def mrp_device(mock_scan): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf("127.0.0.1", "MRP Device", conf.MrpService("mrpid", 5555)) + ) + yield mock_scan + + +@pytest.fixture +def dmap_device(mock_scan): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf( + "127.0.0.1", + "DMAP Device", + conf.DmapService("dmapid", None, port=6666), + ) + ) + yield mock_scan + + +@pytest.fixture +def dmap_device_with_credentials(mock_scan): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf( + "127.0.0.1", + "DMAP Device", + conf.DmapService("dmapid", "dummy_creds", port=6666), + ) + ) + yield mock_scan + + +@pytest.fixture +def airplay_device(mock_scan): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf( + "127.0.0.1", "AirPlay Device", conf.AirPlayService("airplayid", port=7777) + ) + ) + yield mock_scan diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py new file mode 100644 index 00000000000..50344dc3c05 --- /dev/null +++ b/tests/components/apple_tv/test_config_flow.py @@ -0,0 +1,582 @@ +"""Test config flow.""" + +from pyatv import exceptions +from pyatv.const import Protocol +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +DMAP_SERVICE = { + "type": "_touch-able._tcp.local.", + "name": "dmapid.something", + "properties": {"CtlN": "Apple TV"}, +} + + +@pytest.fixture(autouse=True) +def mock_setup_entry(): + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.apple_tv.async_setup_entry", return_value=True + ): + yield + + +# User Flows + + +async def test_user_input_device_not_found(hass, mrp_device): + """Test when user specifies a non-existing device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {"devices": "`MRP Device (127.0.0.1)`"} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "none"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_devices_found"} + + +async def test_user_input_unexpected_error(hass, mock_scan): + """Test that unexpected error yields an error message.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_scan.side_effect = Exception + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "dummy"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_user_adds_full_device(hass, full_device, pairing): + """Test adding device with all services.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"name": "MRP Device"} + + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["description_placeholders"] == {"protocol": "MRP"} + + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": 1111} + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} + + result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result5["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result5["description_placeholders"] == {"protocol": "AirPlay"} + + result6 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": 1234} + ) + assert result6["type"] == "create_entry" + assert result6["data"] == { + "address": "127.0.0.1", + "credentials": { + Protocol.DMAP.value: "dmap_creds", + Protocol.MRP.value: "mrp_creds", + Protocol.AirPlay.value: "airplay_creds", + }, + "name": "MRP Device", + "protocol": Protocol.MRP.value, + } + + +async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing): + """Test adding device with only DMAP service.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "DMAP Device"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"name": "DMAP Device"} + + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["description_placeholders"] == {"pin": 1111, "protocol": "DMAP"} + + result6 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": 1234} + ) + assert result6["type"] == "create_entry" + assert result6["data"] == { + "address": "127.0.0.1", + "credentials": {Protocol.DMAP.value: "dmap_creds"}, + "name": "DMAP Device", + "protocol": Protocol.DMAP.value, + } + + +async def test_user_adds_dmap_device_failed(hass, dmap_device, dmap_pin, pairing): + """Test adding DMAP device where remote device did not attempt to pair.""" + pairing.always_fail = True + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "DMAP Device"}, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "device_did_not_pair" + + +async def test_user_adds_device_with_credentials(hass, dmap_device_with_credentials): + """Test adding DMAP device with existing credentials (home sharing).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "DMAP Device"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"name": "DMAP Device"} + + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result3["type"] == "create_entry" + assert result3["data"] == { + "address": "127.0.0.1", + "credentials": {Protocol.DMAP.value: "dummy_creds"}, + "name": "DMAP Device", + "protocol": Protocol.DMAP.value, + } + + +async def test_user_adds_device_with_ip_filter( + hass, dmap_device_with_credentials, mock_scan +): + """Test add device filtering by IP.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "127.0.0.1"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"name": "DMAP Device"} + + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result3["type"] == "create_entry" + assert result3["data"] == { + "address": "127.0.0.1", + "credentials": {Protocol.DMAP.value: "dummy_creds"}, + "name": "DMAP Device", + "protocol": Protocol.DMAP.value, + } + + +async def test_user_adds_device_by_ip_uses_unicast_scan(hass, mock_scan): + """Test add device by IP-address, verify unicast scan is used.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "127.0.0.1"}, + ) + + assert str(mock_scan.hosts[0]) == "127.0.0.1" + + +async def test_user_adds_existing_device(hass, mrp_device): + """Test that it is not possible to add existing device.""" + MockConfigEntry(domain="apple_tv", unique_id="mrpid").add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "127.0.0.1"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "already_configured"} + + +async def test_user_adds_unusable_device(hass, airplay_device): + """Test that it is not possible to add pure AirPlay device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "AirPlay Device"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_usable_service"} + + +async def test_user_connection_failed(hass, mrp_device, pairing_mock): + """Test error message when connection to device fails.""" + pairing_mock.begin.side_effect = exceptions.ConnectionFailedError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "invalid_config" + + +async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock): + """Test initiating pairing fails.""" + pairing_mock.begin.side_effect = exceptions.PairingError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "invalid_auth" + + +async def test_user_pair_invalid_pin(hass, mrp_device, pairing_mock): + """Test pairing with invalid pin.""" + pairing_mock.finish.side_effect = exceptions.PairingError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"pin": 1111}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_user_pair_unexpected_error(hass, mrp_device, pairing_mock): + """Test unexpected error when entering PIN code.""" + + pairing_mock.finish.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"pin": 1111}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_user_pair_backoff_error(hass, mrp_device, pairing_mock): + """Test that backoff error is displayed in case device requests it.""" + pairing_mock.begin.side_effect = exceptions.BackOffError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "backoff" + + +async def test_user_pair_begin_unexpected_error(hass, mrp_device, pairing_mock): + """Test unexpected error during start of pairing.""" + pairing_mock.begin.side_effect = Exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "MRP Device"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "unknown" + + +# Zeroconf + + +async def test_zeroconf_unsupported_service_aborts(hass): + """Test discovering unsupported zeroconf service.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "type": "_dummy._tcp.local.", + "properties": {}, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): + """Test add MRP device discovered by zeroconf.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "type": "_mediaremotetv._tcp.local.", + "properties": {"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {"name": "MRP Device"} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"protocol": "MRP"} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": 1111} + ) + assert result3["type"] == "create_entry" + assert result3["data"] == { + "address": "127.0.0.1", + "credentials": {Protocol.MRP.value: "mrp_creds"}, + "name": "MRP Device", + "protocol": Protocol.MRP.value, + } + + +async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): + """Test add DMAP device discovered by zeroconf.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {"name": "DMAP Device"} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} + + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result3["type"] == "create_entry" + assert result3["data"] == { + "address": "127.0.0.1", + "credentials": {Protocol.DMAP.value: "dmap_creds"}, + "name": "DMAP Device", + "protocol": Protocol.DMAP.value, + } + + +async def test_zeroconf_add_existing_aborts(hass, dmap_device): + """Test start new zeroconf flow while existing flow is active aborts.""" + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +async def test_zeroconf_add_but_device_not_found(hass, mock_scan): + """Test add device which is not found with another scan.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" + + +async def test_zeroconf_add_existing_device(hass, dmap_device): + """Test add already existing device from zeroconf.""" + MockConfigEntry(domain="apple_tv", unique_id="dmapid").add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_unexpected_error(hass, mock_scan): + """Test unexpected error aborts in zeroconf.""" + mock_scan.side_effect = Exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +# Re-configuration + + +async def test_reconfigure_update_credentials(hass, mrp_device, pairing): + """Test that reconfigure flow updates config entry.""" + config_entry = MockConfigEntry(domain="apple_tv", unique_id="mrpid") + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data={"identifier": "mrpid", "name": "apple tv"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"protocol": "MRP"} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": 1111} + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + + assert config_entry.data == { + "address": "127.0.0.1", + "protocol": Protocol.MRP.value, + "name": "MRP Device", + "credentials": {Protocol.MRP.value: "mrp_creds"}, + } + + +async def test_reconfigure_ongoing_aborts(hass, mrp_device): + """Test start additional reconfigure flow aborts.""" + data = { + "identifier": "mrpid", + "name": "Apple TV", + } + + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=data + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +# Options + + +async def test_option_start_off(hass): + """Test start off-option flag.""" + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="dmapid", options={"start_off": False} + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_START_OFF: True} + ) + assert result2["type"] == "create_entry" + + assert config_entry.options[CONF_START_OFF] From 25db1dac230e1ec72d7ba649a1efa5bde5e54e48 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Dec 2020 17:14:05 +0100 Subject: [PATCH 354/430] Add preset support to deCONZ climate platform (#43722) --- homeassistant/components/deconz/climate.py | 49 ++++++++ tests/components/deconz/test_climate.py | 131 +++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 278d4144b6f..51830387a27 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,4 +1,6 @@ """Support for deCONZ climate devices.""" +from typing import Optional + from pydeconz.sensor import Thermostat from homeassistant.components.climate import DOMAIN, ClimateEntity @@ -7,6 +9,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -24,6 +30,21 @@ HVAC_MODES = { HVAC_MODE_OFF: "off", } +DECONZ_PRESET_AUTO = "auto" +DECONZ_PRESET_COMPLEX = "complex" +DECONZ_PRESET_HOLIDAY = "holiday" +DECONZ_PRESET_MANUAL = "manual" + +PRESET_MODES = { + DECONZ_PRESET_AUTO: "auto", + PRESET_BOOST: "boost", + PRESET_COMFORT: "comfort", + DECONZ_PRESET_COMPLEX: "complex", + PRESET_ECO: "eco", + DECONZ_PRESET_HOLIDAY: "holiday", + DECONZ_PRESET_MANUAL: "manual", +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ climate devices. @@ -82,6 +103,9 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): self._features = SUPPORT_TARGET_TEMPERATURE + if "preset" in device.raw["config"]: + self._features |= SUPPORT_PRESET_MODE + @property def supported_features(self): """Return the list of supported features.""" @@ -120,6 +144,31 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): await self._device.async_set_config(data) + # Preset control + + @property + def preset_mode(self) -> Optional[str]: + """Return preset mode.""" + for hass_preset_mode, preset_mode in PRESET_MODES.items(): + if self._device.preset == preset_mode: + return hass_preset_mode + + return None + + @property + def preset_modes(self) -> list: + """Return the list of available preset modes.""" + return list(PRESET_MODES) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode not in PRESET_MODES: + raise ValueError(f"Unsupported preset mode {preset_mode}") + + data = {"preset": PRESET_MODES[preset_mode]} + + await self._device.async_set_config(data) + # Temperature control @property diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index cea660b855b..0930794afe9 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -7,17 +7,21 @@ import pytest from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, + ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_COMFORT, ) +from homeassistant.components.deconz.climate import DECONZ_PRESET_MANUAL from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, @@ -428,6 +432,133 @@ async def test_climate_device_with_cooling_support(hass): ) +async def test_climate_device_with_preset(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": None, + "heatsetpoint": 2222, + "mode": "heat", + "preset": "auto", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 2 + + climate_zen_01 = hass.states.get("climate.zen_01") + assert climate_zen_01.state == HVAC_MODE_HEAT + assert climate_zen_01.attributes["current_temperature"] == 23.2 + assert climate_zen_01.attributes["temperature"] == 22.2 + assert climate_zen_01.attributes["preset_mode"] == "auto" + assert climate_zen_01.attributes["preset_modes"] == [ + "auto", + "boost", + "comfort", + "complex", + "eco", + "holiday", + "manual", + ] + + # Event signals deCONZ preset + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"preset": "manual"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert ( + hass.states.get("climate.zen_01").attributes["preset_mode"] + == DECONZ_PRESET_MANUAL + ) + + # Event signals unknown preset + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"preset": "unsupported"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["preset_mode"] is None + + # Verify service calls + + thermostat_device = gateway.api.sensors["0"] + + # Service set preset to HASS preset + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"preset": "comfort"} + ) + + # Service set preset to custom deCONZ preset + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"preset": "manual"} + ) + + # Service set preset to unsupported value + + with patch.object( + thermostat_device, "_request", return_value=True + ) as set_callback, pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: "unsupported"}, + blocking=True, + ) + + async def test_clip_climate_device(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) From efacda6c33bfb59a9e4c1bdb2dc0969817b3b348 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Dec 2020 08:47:09 -0800 Subject: [PATCH 355/430] Bump androidtv to 0.0.56 (#43859) --- 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 eaa59f50db8..6adea1af5af 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.54", + "androidtv[async]==0.0.56", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 5a08853fb80..7172e2bf6fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -245,7 +245,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.54 +androidtv[async]==0.0.56 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c4efc55037..5f02d7953c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ airly==1.0.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.54 +androidtv[async]==0.0.56 # homeassistant.components.apns apns2==0.3.0 From 6e8efe2b679f502a7329e19ff72bcae5b0156539 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Dec 2020 18:08:46 +0100 Subject: [PATCH 356/430] Add fan support to deCONZ climate platform (#43721) --- homeassistant/components/deconz/climate.py | 50 ++++++ homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_climate.py | 146 +++++++++++++++++- 5 files changed, 198 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 51830387a27..3e1e1748737 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -5,6 +5,12 @@ from pydeconz.sensor import Thermostat from homeassistant.components.climate import DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -12,6 +18,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -23,6 +30,18 @@ from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry +DECONZ_FAN_SMART = "smart" + +FAN_MODES = { + DECONZ_FAN_SMART: "smart", + FAN_AUTO: "auto", + FAN_HIGH: "high", + FAN_MEDIUM: "medium", + FAN_LOW: "low", + FAN_ON: "on", + FAN_OFF: "off", +} + HVAC_MODES = { HVAC_MODE_AUTO: "auto", HVAC_MODE_COOL: "cool", @@ -103,6 +122,9 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): self._features = SUPPORT_TARGET_TEMPERATURE + if "fanmode" in device.raw["config"]: + self._features |= SUPPORT_FAN_MODE + if "preset" in device.raw["config"]: self._features |= SUPPORT_PRESET_MODE @@ -111,6 +133,34 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Return the list of supported features.""" return self._features + # Fan control + + @property + def fan_mode(self) -> str: + """Return fan operation.""" + for hass_fan_mode, fan_mode in FAN_MODES.items(): + if self._device.fanmode == fan_mode: + return hass_fan_mode + + if self._device.state_on: + return FAN_ON + + return FAN_OFF + + @property + def fan_modes(self) -> list: + """Return the list of available fan operation modes.""" + return list(FAN_MODES) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if fan_mode not in FAN_MODES: + raise ValueError(f"Unsupported fan mode {fan_mode}") + + data = {"fanmode": FAN_MODES[fan_mode]} + + await self._device.async_set_config(data) + # HVAC control @property diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index e9b388e29fe..c2846f8c57f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==75"], + "requirements": ["pydeconz==76"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index 7172e2bf6fe..08325bc3e7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1337,7 +1337,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==75 +pydeconz==76 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f02d7953c9..b9f3f083b10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==75 +pydeconz==76 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 0930794afe9..319675cf6f7 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -6,22 +6,33 @@ import pytest from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_COMFORT, ) -from homeassistant.components.deconz.climate import DECONZ_PRESET_MANUAL +from homeassistant.components.deconz.climate import ( + DECONZ_FAN_SMART, + DECONZ_PRESET_MANUAL, +) from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, @@ -432,6 +443,139 @@ async def test_climate_device_with_cooling_support(hass): ) +async def test_climate_device_with_fan_support(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": "auto", + "heatsetpoint": 2222, + "mode": "heat", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 2 + climate_thermostat = hass.states.get("climate.zen_01") + assert climate_thermostat.state == HVAC_MODE_HEAT + assert climate_thermostat.attributes["fan_mode"] == FAN_AUTO + assert climate_thermostat.attributes["fan_modes"] == [ + DECONZ_FAN_SMART, + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_LOW, + FAN_ON, + FAN_OFF, + ] + + # Event signals fan mode defaults to off + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"fanmode": "unsupported"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF + + # Event signals unsupported fan mode + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"fanmode": "unsupported"}, + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + + # Event signals unsupported fan mode + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"fanmode": "unsupported"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + + # Verify service calls + + thermostat_device = gateway.api.sensors["0"] + + # Service set fan mode to off + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF}, + blocking=True, + ) + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"fanmode": "off"} + ) + + # Service set fan mode to custom deCONZ mode smart + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART}, + blocking=True, + ) + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"fanmode": "smart"} + ) + + # Service set fan mode to unsupported value + + with patch.object( + thermostat_device, "_request", return_value=True + ) as set_callback, pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: "unsupported"}, + blocking=True, + ) + + async def test_climate_device_with_preset(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) From f2f935506e301a0313067273651899380ca5900f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Dec 2020 12:00:13 -0600 Subject: [PATCH 357/430] Use Plex websocket payloads to reduce overhead (#42332) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + homeassistant/components/plex/__init__.py | 11 +- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/media_player.py | 460 +++++++----------- homeassistant/components/plex/models.py | 126 +++++ homeassistant/components/plex/sensor.py | 101 +--- homeassistant/components/plex/server.py | 134 ++++- tests/components/plex/conftest.py | 3 + tests/components/plex/helpers.py | 39 +- tests/components/plex/mock_classes.py | 1 + tests/components/plex/test_browse_media.py | 4 - tests/components/plex/test_config_flow.py | 12 +- tests/components/plex/test_init.py | 14 +- tests/components/plex/test_media_players.py | 23 +- tests/components/plex/test_server.py | 50 +- 15 files changed, 516 insertions(+), 464 deletions(-) create mode 100644 homeassistant/components/plex/models.py diff --git a/.coveragerc b/.coveragerc index 960850fa99e..e5ee7b3dc92 100644 --- a/.coveragerc +++ b/.coveragerc @@ -690,6 +690,7 @@ omit = homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* homeassistant/components/plex/media_player.py + homeassistant/components/plex/models.py homeassistant/components/plex/sensor.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index e4f4f80dcfa..de8b278f3cf 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,6 +1,5 @@ """Support to embed Plex.""" import asyncio -import functools from functools import partial import logging @@ -35,10 +34,7 @@ from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CONF_SERVER, @@ -176,6 +172,7 @@ async def async_setup_entry(hass, entry): if data == STATE_CONNECTED: _LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER]) + hass.async_create_task(async_update_plex()) elif data == STATE_DISCONNECTED: _LOGGER.debug( "Websocket to %s disconnected, retrying", entry.data[CONF_SERVER] @@ -190,7 +187,7 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) elif signal == SIGNAL_DATA: - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + hass.async_create_task(plex_server.async_update_session(data)) session = async_get_clientsession(hass) verify_ssl = server_config.get(CONF_VERIFY_SSL) @@ -219,7 +216,7 @@ async def async_setup_entry(hass, entry): task = hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) - task.add_done_callback(functools.partial(start_websocket_session, platform)) + task.add_done_callback(partial(start_websocket_session, platform)) async def async_play_on_sonos_service(service_call): await hass.async_add_executor_job(play_on_sonos, hass, service_call) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index f54c376c667..c13be439be7 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -24,6 +24,7 @@ WEBSOCKETS = "websockets" PLEX_SERVER_CONFIG = "server_config" PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" +PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL = "plex_update_session_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.{}" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 295254aa612..4d765cc0508 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -1,4 +1,5 @@ """Support to interface with the Plex API.""" +from functools import wraps import json import logging @@ -7,10 +8,7 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerEntity from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -22,12 +20,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING 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 homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.network import is_internal_request -from homeassistant.util import dt as dt_util from .const import ( COMMON_PLAYERS, @@ -36,23 +36,28 @@ from .const import ( DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, + PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) from .media_browser import browse_media -LIVE_TV_SECTION = "-4" -PLAYLISTS_BROWSE_PAYLOAD = { - "title": "Playlists", - "media_content_id": "all", - "media_content_type": "playlists", - "can_play": False, - "can_expand": True, -} - _LOGGER = logging.getLogger(__name__) +def needs_session(func): + """Ensure session is available for certain attributes.""" + + @wraps(func) + def get_session_attribute(self, *args): + if self.session is None: + return None + return func(self, *args) + + return get_session_attribute + + 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] @@ -60,9 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_new_media_players(new_entities): - _async_add_entities( - hass, registry, config_entry, async_add_entities, server_id, new_entities - ) + _async_add_entities(hass, registry, async_add_entities, server_id, new_entities) unsub = async_dispatcher_connect( hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players @@ -72,9 +75,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def _async_add_entities( - hass, registry, config_entry, async_add_entities, server_id, new_entities -): +def _async_add_entities(hass, registry, async_add_entities, server_id, new_entities): """Set up Plex media_player entities.""" _LOGGER.debug("New entities: %s", new_entities) entities = [] @@ -106,258 +107,113 @@ class PlexMediaPlayer(MediaPlayerEntity): """Initialize the Plex device.""" self.plex_server = plex_server self.device = device - self.session = session self.player_source = player_source - self._app_name = "" + + self.device_make = None + self.device_platform = None + self.device_product = None + self.device_title = None + self.device_version = None + self.machine_identifier = device.machineIdentifier + self.session_device = None + self._available = False self._device_protocol_capabilities = None - self._is_player_active = False - self._machine_identifier = device.machineIdentifier - self._make = "" - self._device_platform = None - self._device_product = None - self._device_title = None - self._device_version = None self._name = None - self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting - 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 - # General - self._media_content_id = None - self._media_content_rating = None - self._media_content_type = None - self._media_duration = None - self._media_image_url = None - self._media_summary = None - self._media_title = None - self._media_position = None - self._media_position_updated_at = None - # Music - self._media_album_artist = None - self._media_album_name = None - self._media_artist = None - self._media_track = None - # TV Show - self._media_episode = None - self._media_season = None - self._media_series_title = None + + # Initializes other attributes + self.session = session 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), - self.async_refresh_media_player, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), + self.async_refresh_media_player, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(self.unique_id), + self.async_update_from_websocket, + ) ) - self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) @callback - def async_refresh_media_player(self, device, session): + def async_refresh_media_player(self, device, session, source): """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 + if source: + self.player_source = source self.async_schedule_update_ha_state(True) - def _clear_media_details(self): - """Set all Media Items to None.""" - # General - self._media_content_id = None - self._media_content_rating = None - self._media_content_type = None - self._media_duration = None - self._media_image_url = None - self._media_summary = None - self._media_title = None - # Music - self._media_album_artist = None - self._media_album_name = None - self._media_artist = None - self._media_track = None - # TV Show - self._media_episode = None - self._media_season = None - self._media_series_title = None + async_dispatcher_send( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier), + ) - # Clear library Name - self._app_name = "" + @callback + def async_update_from_websocket(self, state): + """Update the entity based on new websocket data.""" + self.update_state(state) + self.async_write_ha_state() + + async_dispatcher_send( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier), + ) def update(self): """Refresh key device data.""" - self._clear_media_details() - - self._available = self.device or self.session - - if self.device: - try: - 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._device_platform = self.device.platform - self._device_product = self.device.product - self._device_title = self.device.title - self._device_version = self.device.version - 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 - ), - None, - ) - if session_device: - self._make = session_device.device or "" - self._player_state = session_device.state - self._device_platform = self._device_platform or session_device.platform - self._device_product = self._device_product or session_device.product - self._device_title = self._device_title or session_device.title - self._device_version = self._device_version or session_device.version - else: - _LOGGER.warning("No player associated with active session") + if not self.device: + self._available = False + return - if self.session.usernames: - self._session_username = self.session.usernames[0] + self._available = True - # Calculate throttled position for proper progress display. - position = int(self.session.viewOffset / 1000) - now = dt_util.utcnow() - if self._media_position is not None: - pos_diff = position - self._media_position - time_diff = now - self._media_position_updated_at - if pos_diff != 0 and abs(time_diff.total_seconds() - pos_diff) > 5: - self._media_position_updated_at = now - self._media_position = position - else: - self._media_position_updated_at = now - self._media_position = position + try: + 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._device_protocol_capabilities = self.device.protocolCapabilities - self._media_content_id = self.session.ratingKey - self._media_content_rating = getattr(self.session, "contentRating", None) + for device in filter(None, [self.device, self.session_device]): + self.device_make = self.device_make or device.device + self.device_platform = self.device_platform or device.platform + self.device_product = self.device_product or device.product + self.device_title = self.device_title or device.title + self.device_version = self.device_version or device.version - name_parts = [self._device_product, self._device_title or self._device_platform] - if (self._device_product in COMMON_PLAYERS) and self.make: + name_parts = [self.device_product, self.device_title or self.device_platform] + if (self.device_product in COMMON_PLAYERS) and self.device_make: # Add more context in name for likely duplicates - name_parts.append(self.make) + name_parts.append(self.device_make) if self.username and self.username != self.plex_server.owner: # Prepend username for shared/managed clients name_parts.insert(0, self.username) self._name = NAME_FORMAT.format(" - ".join(name_parts)) - self._set_player_state() - - if self._is_player_active and self.session is not None: - self._session_type = self.session.type - if self.session.duration: - self._media_duration = int(self.session.duration / 1000) - else: - self._media_duration = None - # title (movie name, tv episode name, music song name) - self._media_summary = self.session.summary - self._media_title = self.session.title - # media type - self._set_media_type() - if self.session.librarySectionID == LIVE_TV_SECTION: - self._app_name = "Live TV" - else: - self._app_name = ( - self.session.section().title - if self.session.section() is not None - else "" - ) - self._set_media_image() - else: - self._session_type = None - - def _set_media_image(self): - thumb_url = self.session.thumbUrl - if ( - self.media_content_type is MEDIA_TYPE_TVSHOW - and not self.plex_server.option_use_episode_art - ): - if self.session.librarySectionID == LIVE_TV_SECTION: - thumb_url = self.session.grandparentThumb - else: - 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.name - ) - thumb_url = self.session.url(self.session.art) - - self._media_image_url = thumb_url - - def _set_player_state(self): - if self._player_state == "playing": - self._is_player_active = True - self._state = STATE_PLAYING - elif self._player_state == "paused": - self._is_player_active = True - self._state = STATE_PAUSED - elif self.device: - self._is_player_active = False - self._state = STATE_IDLE - else: - self._is_player_active = False - self._state = STATE_OFF - - def _set_media_type(self): - if self._session_type == "episode": - self._media_content_type = MEDIA_TYPE_TVSHOW - - # season number (00) - self._media_season = self.session.seasonNumber - # show name - self._media_series_title = self.session.grandparentTitle - # episode number (00) - if self.session.index is not None: - self._media_episode = self.session.index - - 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 += f" ({self.session.year!s})" - - 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 - # 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.name, - ) - self._media_artist = self._media_album_artist - - elif self._session_type == "clip": - _LOGGER.debug( - "Clip content type detected, compatibility may vary: %s", self.name - ) - self._media_content_type = MEDIA_TYPE_VIDEO def force_idle(self): """Force client to idle.""" - self._player_state = STATE_IDLE self._state = STATE_IDLE - self.session = None - self._clear_media_details() + if self.player_source == "session": + self.device = None + self.session_device = None + self._available = False @property def should_poll(self): @@ -367,12 +223,21 @@ class PlexMediaPlayer(MediaPlayerEntity): @property def unique_id(self): """Return the id of this plex client.""" - return f"{self.plex_server.machine_identifier}:{self._machine_identifier}" + 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 + def session(self): + """Return the active session for this player.""" + return self._session + + @session.setter + def session(self, session): + self._session = session + if session: + self.session_device = self.session.player + self.update_state(self.session.state) + else: + self._state = STATE_IDLE @property def available(self): @@ -385,20 +250,33 @@ class PlexMediaPlayer(MediaPlayerEntity): return self._name @property + @needs_session def username(self): """Return the username of the client owner.""" - return self._session_username - - @property - def app_name(self): - """Return the library name of playing media.""" - return self._app_name + return self.session.username @property def state(self): """Return the state of the device.""" return self._state + def update_state(self, state): + """Set the state of the device, handle session termination.""" + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED + elif state == "stopped": + self.session = None + self.force_idle() + else: + self._state = STATE_IDLE + + @property + def _is_player_active(self): + """Report if the client is playing media.""" + return self.state in [STATE_PLAYING, STATE_PAUSED] + @property def _active_media_plexapi_type(self): """Get the active media type required by PlexAPI commands.""" @@ -408,84 +286,112 @@ class PlexMediaPlayer(MediaPlayerEntity): return "video" @property + @needs_session + def session_key(self): + """Return current session key.""" + return self.session.sessionKey + + @property + @needs_session + def media_library_title(self): + """Return the library name of playing media.""" + return self.session.media_library_title + + @property + @needs_session def media_content_id(self): """Return the content ID of current playing media.""" - return self._media_content_id + return self.session.media_content_id @property + @needs_session def media_content_type(self): """Return the content type of current playing media.""" - return self._media_content_type + return self.session.media_content_type @property + @needs_session + def media_content_rating(self): + """Return the content rating of current playing media.""" + return self.session.media_content_rating + + @property + @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" - return self._media_artist + return self.session.media_artist @property + @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" - return self._media_album_name + return self.session.media_album_name @property + @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" - return self._media_album_artist + return self.session.media_album_artist @property + @needs_session def media_track(self): """Return the track number of current playing media, music only.""" - return self._media_track + return self.session.media_track @property + @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" - return self._media_duration + return self.session.media_duration @property + @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" - return self._media_position + return self.session.media_position @property + @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" - return self._media_position_updated_at + return self.session.media_position_updated_at @property + @needs_session def media_image_url(self): """Return the image URL of current playing media.""" - return self._media_image_url + return self.session.media_image_url @property + @needs_session def media_summary(self): """Return the summary of current playing media.""" - return self._media_summary + return self.session.media_summary @property + @needs_session def media_title(self): """Return the title of current playing media.""" - return self._media_title + return self.session.media_title @property + @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" - return self._media_season + return self.session.media_season @property + @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" - return self._media_series_title + return self.session.media_series_title @property + @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" - return self._media_episode - - @property - def make(self): - """Return the make of the device (ex. SHIELD Android TV).""" - return self._make + return self.session.media_episode @property def supported_features(self): @@ -521,12 +427,14 @@ class PlexMediaPlayer(MediaPlayerEntity): and "playback" in self._device_protocol_capabilities ): return self._volume_level + return None @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" if self._is_player_active and self.device: return self._volume_muted + return None def mute_volume(self, mute): """Mute the volume. @@ -605,13 +513,19 @@ class PlexMediaPlayer(MediaPlayerEntity): @property def device_state_attributes(self): """Return the scene state attributes.""" - return { - "media_content_rating": self._media_content_rating, - "session_username": self.username, - "media_library_name": self._app_name, - "summary": self.media_summary, - "player_source": self.player_source, - } + attributes = {} + for attr in [ + "media_content_rating", + "media_library_title", + "player_source", + "summary", + "username", + ]: + value = getattr(self, attr, None) + if value: + attributes[attr] = value + + return attributes @property def device_info(self): @@ -621,10 +535,10 @@ class PlexMediaPlayer(MediaPlayerEntity): return { "identifiers": {(PLEX_DOMAIN, self.machine_identifier)}, - "manufacturer": self._device_platform or "Plex", - "model": self._device_product or self.make, + "manufacturer": self.device_platform or "Plex", + "model": self.device_product or self.device_make, "name": self.name, - "sw_version": self._device_version, + "sw_version": self.device_version, "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier), } diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py new file mode 100644 index 00000000000..7633c5deaa8 --- /dev/null +++ b/homeassistant/components/plex/models.py @@ -0,0 +1,126 @@ +"""Models to represent various Plex objects used in the integration.""" +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, +) +from homeassistant.util import dt as dt_util + +LIVE_TV_SECTION = "-4" + + +class PlexSession: + """Represents a Plex playback session.""" + + def __init__(self, plex_server, session): + """Initialize the object.""" + self.plex_server = plex_server + + # Available on both media and session objects + self.media_content_id = None + self.media_content_type = None + self.media_content_rating = None + self.media_duration = None + self.media_image_url = None + self.media_library_title = None + self.media_summary = None + self.media_title = None + # TV Shows + self.media_episode = None + self.media_season = None + self.media_series_title = None + # Music + self.media_album_name = None + self.media_album_artist = None + self.media_artist = None + self.media_track = None + + # Only available on sessions + self.player = next(iter(session.players), None) + self.device_product = self.player.product + self.media_position = session.viewOffset + self.session_key = session.sessionKey + self.state = self.player.state + self.username = next(iter(session.usernames), None) + + # Used by sensor entity + sensor_user_list = [self.username, self.device_product] + self.sensor_title = None + self.sensor_user = " - ".join(filter(None, sensor_user_list)) + + self.update_media(session) + + def __repr__(self): + """Return representation of the session.""" + return f"<{self.session_key}:{self.sensor_title}>" + + def update_media(self, media): + """Update attributes from a media object.""" + self.media_content_id = media.ratingKey + self.media_content_rating = getattr(media, "contentRating", None) + self.media_image_url = self.get_media_image_url(media) + self.media_summary = media.summary + self.media_title = media.title + + if media.duration: + self.media_duration = int(media.duration / 1000) + + if media.librarySectionID == LIVE_TV_SECTION: + self.media_library_title = "Live TV" + else: + self.media_library_title = ( + media.section().title if media.section() is not None else "" + ) + + if media.type == "episode": + self.media_content_type = MEDIA_TYPE_TVSHOW + self.media_season = media.seasonNumber + self.media_series_title = media.grandparentTitle + if media.index is not None: + self.media_episode = media.index + self.sensor_title = f"{self.media_series_title} - {media.seasonEpisode} - {self.media_title}" + elif media.type == "movie": + self.media_content_type = MEDIA_TYPE_MOVIE + if media.year is not None and media.title is not None: + self.media_title += f" ({media.year!s})" + self.sensor_title = self.media_title + elif media.type == "track": + self.media_content_type = MEDIA_TYPE_MUSIC + self.media_album_name = media.parentTitle + self.media_album_artist = media.grandparentTitle + self.media_track = media.index + self.media_artist = media.originalTitle or self.media_album_artist + self.sensor_title = ( + f"{self.media_artist} - {self.media_album_name} - {self.media_title}" + ) + elif media.type == "clip": + self.media_content_type = MEDIA_TYPE_VIDEO + self.sensor_title = media.title + else: + self.sensor_title = "Unknown" + + @property + def media_position(self): + """Return the current playback position.""" + return self._media_position + + @media_position.setter + def media_position(self, offset): + """Set the current playback position.""" + self._media_position = int(offset / 1000) + self.media_position_updated_at = dt_util.utcnow() + + def get_media_image_url(self, media): + """Get the image URL from a media object.""" + thumb_url = media.thumbUrl + if media.type == "episode" and not self.plex_server.option_use_episode_art: + if media.librarySectionID == LIVE_TV_SECTION: + thumb_url = media.grandparentThumb + else: + thumb_url = media.url(media.grandparentThumb) + + if thumb_url is None: + thumb_url = media.url(media.art) + + return thumb_url diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index a3b465dfdb0..8c3733a7450 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,20 +1,15 @@ """Support for Plex media server monitoring.""" import logging -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_call_later from .const import ( CONF_SERVER_IDENTIFIER, DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_UPDATE_PLATFORMS_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) @@ -26,21 +21,26 @@ 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] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] - sensor = PlexSensor(plexserver) + sensor = PlexSensor(hass, plexserver) async_add_entities([sensor]) class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__(self, plex_server): + def __init__(self, hass, plex_server): """Initialize the sensor.""" - self.sessions = [] self._state = None - self._now_playing = [] self._server = plex_server self._name = NAME_FORMAT.format(plex_server.friendly_name) self._unique_id = f"sensor-{plex_server.machine_identifier}" + self.async_refresh_sensor = Debouncer( + hass, + _LOGGER, + cooldown=3, + immediate=False, + function=self._async_refresh_sensor, + ).async_call async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -52,85 +52,12 @@ class PlexSensor(Entity): ) self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) - async def async_refresh_sensor(self, sessions): + async def _async_refresh_sensor(self): """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) - - self.sessions = sessions - update_failed = False - - @callback - def update_plex(_): - async_dispatcher_send( - self.hass, - PLEX_UPDATE_PLATFORMS_SIGNAL.format(self._server.machine_identifier), - ) - - now_playing = [] - for sess in self.sessions: - if sess.TYPE == "photo": - _LOGGER.debug("Photo session detected, skipping: %s", sess) - continue - if not sess.usernames: - _LOGGER.debug( - "Session temporarily incomplete, will try again: %s", sess - ) - update_failed = True - continue - user = sess.usernames[0] - device = sess.players[0].title - now_playing_user = f"{user} - {device}" - now_playing_title = "" - - if sess.TYPE == "episode": - # example: - # "Supernatural (2005) - s01e13 - Route 666" - - def sync_io_attributes(session): - year = None - try: - year = session.show().year - except TypeError: - pass - return (year, session.seasonEpisode) - - year, season_episode = await self.hass.async_add_executor_job( - sync_io_attributes, sess - ) - season_title = sess.grandparentTitle - if year is not None: - season_title += f" ({year!s})" - episode_title = sess.title - now_playing_title = ( - f"{season_title} - {season_episode} - {episode_title}" - ) - elif sess.TYPE == "track": - # example: - # "Billy Talent - Afraid of Heights - Afraid of Heights" - track_artist = sess.grandparentTitle - track_album = sess.parentTitle - track_title = sess.title - now_playing_title = f"{track_artist} - {track_album} - {track_title}" - elif sess.TYPE == "movie": - # example: - # "picture_of_last_summer_camp (2015)" - # "The Incredible Hulk (2008)" - now_playing_title = sess.title - year = await self.hass.async_add_executor_job(getattr, sess, "year") - if year is not None: - now_playing_title += f" ({year})" - else: - now_playing_title = sess.title - - now_playing.append((now_playing_user, now_playing_title)) - self._state = len(self.sessions) - self._now_playing = now_playing - + self._state = len(self._server.sensor_attributes) self.async_write_ha_state() - if update_failed: - async_call_later(self.hass, 5, update_plex) - @property def name(self): """Return the name of the sensor.""" @@ -164,7 +91,7 @@ class PlexSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return {content[0]: content[1] for content in self._now_playing} + return self._server.sensor_attributes @property def device_info(self): diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index ed84b0c5d8f..3834833b740 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -37,6 +37,7 @@ from .const import ( GDM_SCANNER, PLAYER_SOURCE, PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, PLEXTV_THROTTLE, @@ -52,6 +53,7 @@ from .errors import ( ShouldUpdateConfigEntry, ) from .media_search import lookup_movie, lookup_music, lookup_tv +from .models import PlexSession _LOGGER = logging.getLogger(__name__) @@ -71,6 +73,7 @@ class PlexServer: """Initialize a Plex server instance.""" self.hass = hass self.entry_id = entry_id + self.active_sessions = {} self._plex_account = None self._plex_server = None self._created_clients = set() @@ -233,7 +236,7 @@ class PlexServer: raise ShouldUpdateConfigEntry @callback - def async_refresh_entity(self, machine_identifier, device, session): + def async_refresh_entity(self, machine_identifier, device, session, source): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" _LOGGER.debug("Refreshing %s", unique_id) @@ -242,6 +245,64 @@ class PlexServer: PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, session, + source, + ) + + async def async_update_session(self, payload): + """Process a session payload received from a websocket callback.""" + try: + session_payload = payload["PlaySessionStateNotification"][0] + except KeyError: + await self.async_update_platforms() + return + + state = session_payload["state"] + if state == "buffering": + return + + session_key = int(session_payload["sessionKey"]) + offset = int(session_payload["viewOffset"]) + rating_key = int(session_payload["ratingKey"]) + + unique_id, active_session = next( + ( + (unique_id, session) + for unique_id, session in self.active_sessions.items() + if session.session_key == session_key + ), + (None, None), + ) + + if not active_session: + await self.async_update_platforms() + return + + if state == "stopped": + self.active_sessions.pop(unique_id, None) + else: + active_session.state = state + active_session.media_position = offset + + def update_with_new_media(): + """Update an existing session with new media details.""" + media = self.fetch_item(rating_key) + active_session.update_media(media) + + if active_session.media_content_id != rating_key and state in [ + "playing", + "paused", + ]: + await self.hass.async_add_executor_job(update_with_new_media) + + async_dispatcher_send( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(unique_id), + state, + ) + + async_dispatcher_send( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), ) def _fetch_platform_data(self): @@ -322,9 +383,6 @@ class PlexServer: device.machineIdentifier, ) - for device in devices: - process_device("PMS", device) - def connect_to_client(source, baseurl, machine_identifier, name="Unknown"): """Connect to a Plex client and return a PlexClient instance.""" try: @@ -385,25 +443,46 @@ class PlexServer: elif plextv_client.clientIdentifier not in available_clients: connect_to_resource(plextv_client) - await self.hass.async_add_executor_job(connect_new_clients) + def process_sessions(): + live_session_keys = {x.sessionKey for x in sessions} + for unique_id, session in list(self.active_sessions.items()): + if session.session_key not in live_session_keys: + _LOGGER.debug("Purging unknown session: %s", session.session_key) + self.active_sessions.pop(unique_id) - for session in sessions: - if session.TYPE == "photo": - _LOGGER.debug("Photo session detected, skipping: %s", session) - continue - - session_username = session.usernames[0] - for player in session.players: - if session_username and session_username not in monitored_users: - ignored_clients.add(player.machineIdentifier) - _LOGGER.debug( - "Ignoring %s client owned by '%s'", - player.product, - session_username, - ) + for session in sessions: + if session.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", session) continue - process_device("session", player) - available_clients[player.machineIdentifier]["session"] = session + + session_username = session.usernames[0] + for player in session.players: + unique_id = f"{self.machine_identifier}:{player.machineIdentifier}" + if unique_id not in self.active_sessions: + _LOGGER.debug("Creating new Plex session: %s", session) + self.active_sessions[unique_id] = PlexSession(self, session) + if session_username and session_username not in monitored_users: + ignored_clients.add(player.machineIdentifier) + _LOGGER.debug( + "Ignoring %s client owned by '%s'", + player.product, + session_username, + ) + continue + + process_device("session", player) + available_clients[player.machineIdentifier][ + "session" + ] = self.active_sessions[unique_id] + + for device in devices: + process_device("PMS", device) + + def sync_tasks(): + connect_new_clients() + process_sessions() + + await self.hass.async_add_executor_job(sync_tasks) new_entity_configs = [] for client_id, client_data in available_clients.items(): @@ -414,7 +493,10 @@ class PlexServer: self._created_clients.add(client_id) else: self.async_refresh_entity( - client_id, client_data["device"], client_data.get("session") + client_id, + client_data["device"], + client_data.get("session"), + client_data.get(PLAYER_SOURCE), ) self._known_clients.update(new_clients | ignored_clients) @@ -423,7 +505,7 @@ class PlexServer: self._known_clients - self._known_idle - ignored_clients ).difference(available_clients) for client_id in idle_clients: - self.async_refresh_entity(client_id, None, None) + self.async_refresh_entity(client_id, None, None, None) self._known_idle.add(client_id) self._client_device_cache.pop(client_id, None) @@ -437,7 +519,6 @@ class PlexServer: async_dispatcher_send( self.hass, PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), - sessions, ) @property @@ -572,3 +653,8 @@ class PlexServer: except MediaNotFound as failed_item: _LOGGER.error("%s not found in %s", failed_item, library_name) return None + + @property + def sensor_attributes(self): + """Return active session information for use in activity sensor.""" + return {x.sensor_user: x.sensor_title for x in self.active_sessions.values()} diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index b3fc235bfc8..1838df32a05 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.plex.const import DOMAIN from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .helpers import websocket_connected from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer from tests.async_mock import patch @@ -52,6 +53,8 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + websocket_connected(mock_websocket) + await hass.async_block_till_done() return plex_server return _wrapper diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index a20d70fbb7e..2fca88fae27 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -1,8 +1,39 @@ """Helper methods for Plex tests.""" -from plexwebsocket import SIGNAL_DATA +from datetime import timedelta + +from plexwebsocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA, STATE_CONNECTED + +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed + +UPDATE_PAYLOAD = { + "PlaySessionStateNotification": [ + { + "sessionKey": "999", + "ratingKey": "12345", + "viewOffset": 5050, + "playQueueItemID": 54321, + "state": "playing", + } + ] +} -def trigger_plex_update(mock_websocket): - """Call the websocket callback method.""" +def websocket_connected(mock_websocket): + """Call the websocket callback method to signal successful connection.""" callback = mock_websocket.call_args[0][1] - callback(SIGNAL_DATA, None, None) + callback(SIGNAL_CONNECTION_STATE, STATE_CONNECTED, None) + + +def trigger_plex_update(mock_websocket, payload=UPDATE_PAYLOAD): + """Call the websocket callback method with a Plex update.""" + callback = mock_websocket.call_args[0][1] + callback(SIGNAL_DATA, payload, None) + + +async def wait_for_debouncer(hass): + """Move time forward to wait for sensor debouncer.""" + next_update = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 13fe4c4113b..8ac894438be 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -334,6 +334,7 @@ class MockPlexSession: self.usernames = [list(MOCK_USERS)[index]] self.players = [player] self._section = MockPlexLibrarySection("Movies") + self.sessionKey = index + 1 @property def duration(self): diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index d96fdd4a00b..66cbc51ef82 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -8,16 +8,12 @@ from homeassistant.components.plex.media_browser import SPECIAL_METHODS from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT from .const import DEFAULT_DATA -from .helpers import trigger_plex_update async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket): """Test getting Plex clients from plex.tv.""" websocket_client = await hass_ws_client(hass) - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - media_players = hass.states.async_entity_ids("media_player") msg_id = 1 diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index a7a2896d307..d8c010ceb92 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ) from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN -from .helpers import trigger_plex_update +from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import ( MockGDM, MockPlexAccount, @@ -440,10 +440,10 @@ async def test_option_flow_new_users_available( OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}} entry.options = OPTIONS_OWNER_ONLY - mock_plex_server = await setup_plex_server(config_entry=entry, disable_gdm=False) - with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient): - trigger_plex_update(mock_websocket) + mock_plex_server = await setup_plex_server( + config_entry=entry, disable_gdm=False + ) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier @@ -453,6 +453,8 @@ async def test_option_flow_new_users_available( assert len(monitored_users) == 1 assert len(new_users) == 2 + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -754,7 +756,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized): trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + await wait_for_debouncer(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state != ENTRY_STATE_LOADED diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 13e33791459..a1a159010ef 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -13,11 +13,11 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE import homeassistant.util.dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .helpers import trigger_plex_update +from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer from tests.async_mock import patch @@ -91,19 +91,19 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server): """Test setup component with config.""" - mock_plex_server = await setup_plex_server(config_entry=entry, session_type="photo") + await setup_plex_server(session_type="photo") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - - trigger_plex_update(mock_websocket) await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") - assert media_player.state == "idle" + assert media_player.state == STATE_IDLE + + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == "0" async def test_setup_when_certificate_changed(hass, entry): diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 94703d5dfb3..a4bda5467e2 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -8,45 +8,30 @@ from tests.async_mock import patch async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server): """Test getting Plex clients from plex.tv.""" - mock_plex_server = await setup_plex_server() - server_id = mock_plex_server.machineIdentifier - plex_server = hass.data[DOMAIN][SERVERS][server_id] - resource = next( x for x in mock_plex_account.resources() if x.name.startswith("plex.tv Resource Player") ) with patch.object(resource, "connect", side_effect=NotFound): - await plex_server._async_update_platforms() + mock_plex_server = await setup_plex_server() await hass.async_block_till_done() + server_id = mock_plex_server.machineIdentifier + plex_server = hass.data[DOMAIN][SERVERS][server_id] media_players_before = len(hass.states.async_entity_ids("media_player")) # Ensure one more client is discovered await hass.config_entries.async_unload(entry.entry_id) - mock_plex_server = await setup_plex_server() plex_server = hass.data[DOMAIN][SERVERS][server_id] - - await plex_server._async_update_platforms() - await hass.async_block_till_done() - media_players_after = len(hass.states.async_entity_ids("media_player")) assert media_players_after == media_players_before + 1 # Ensure only plex.tv resource client is found await hass.config_entries.async_unload(entry.entry_id) - - mock_plex_server = await setup_plex_server() - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() - + mock_plex_server = await setup_plex_server(num_users=0) plex_server = hass.data[DOMAIN][SERVERS][server_id] - - await plex_server._async_update_platforms() - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("media_player")) == 1 # Ensure cache gets called diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 8ac707c393f..99a324786f6 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -26,9 +26,8 @@ from homeassistant.components.plex.const import ( from homeassistant.const import ATTR_ENTITY_ID from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .helpers import trigger_plex_update +from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import ( - MockGDM, MockPlexAccount, MockPlexAlbum, MockPlexArtist, @@ -54,15 +53,14 @@ async def test_new_users_available(hass, entry, mock_websocket, setup_plex_serve server_id = mock_plex_server.machineIdentifier - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users ignored_users = [x for x in monitored_users if not monitored_users[x]["enabled"]] assert len(monitored_users) == 1 assert len(ignored_users) == 0 + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -81,9 +79,6 @@ async def test_new_ignored_users_available( server_id = mock_plex_server.machineIdentifier - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users ignored_users = [x for x in mock_plex_server.accounts if x not in monitored_users] @@ -100,6 +95,8 @@ async def test_new_ignored_users_available( in caplog.text ) + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -111,8 +108,7 @@ async def test_network_error_during_refresh( server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -128,14 +124,14 @@ async def test_network_error_during_refresh( async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server): """Test connection failure to a GDM discovered client.""" - mock_plex_server = await setup_plex_server(disable_gdm=False) - with patch( "homeassistant.components.plex.server.PlexClient", side_effect=ConnectionError ): - trigger_plex_update(mock_websocket) + mock_plex_server = await setup_plex_server(disable_gdm=False) await hass.async_block_till_done() + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -146,11 +142,7 @@ async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server): async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): """Test marking media_players as idle when sessions end.""" - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[DOMAIN][SERVERS][server_id] - - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -158,30 +150,23 @@ async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): mock_plex_server.clear_clients() mock_plex_server.clear_sessions() - await loaded_server._async_update_platforms() + trigger_plex_update(mock_websocket) await hass.async_block_till_done() + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == "0" -async def test_ignore_plex_web_client(hass, entry, mock_websocket): +async def test_ignore_plex_web_client(hass, entry, mock_websocket, setup_plex_server): """Test option to ignore Plex Web clients.""" OPTIONS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True entry.options = OPTIONS - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) - ), patch("homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)): + mock_plex_server = await setup_plex_server(config_entry=entry) + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -197,9 +182,6 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - media_player_id = hass.states.async_entity_ids("media_player")[0] with patch("homeassistant.components.plex.PlexServer.create_playqueue"): assert await hass.services.async_call( From a8f0ad1dd868346d9b79bb93c956b7d918f5144b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Dec 2020 19:10:47 +0100 Subject: [PATCH 358/430] Allow creating deCONZ config entry even when no bridge id is available --- .../components/deconz/config_flow.py | 3 -- tests/components/deconz/test_config_flow.py | 41 ------------------- 2 files changed, 44 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c43c1c95504..6c2df3ad614 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -172,9 +172,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") - if self.bridge_id == "0000000000000000": - return self.async_abort(reason="no_hardware_available") - return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) async def async_step_ssdp(self, discovery_info): diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index dd477e76e7f..d922dffb623 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -402,47 +402,6 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): } -async def test_flow_ssdp_discovery_bad_bridge_id_aborts(hass, aioclient_mock): - """Test that config flow aborts if deCONZ signals no radio hardware available.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={ - ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", - ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BAD_BRIDGEID, - }, - context={"source": SOURCE_SSDP}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "link" - - aioclient_mock.post( - "http://1.2.3.4:80/api", - json=[{"success": {"username": API_KEY}}], - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "no_hardware_available" - - -async def test_ssdp_discovery_not_deconz_bridge(hass): - """Test a non deconz bridge being discovered over ssdp.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={ATTR_UPNP_MANUFACTURER_URL: "not deconz bridge"}, - context={"source": SOURCE_SSDP}, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "not_deconz_bridge" - - async def test_ssdp_discovery_update_configuration(hass): """Test if a discovered bridge is configured but updates with new attributes.""" config_entry = await setup_deconz_integration(hass) From 519ec64522a78ff6d3b88a4280428eb6493c332c Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 2 Dec 2020 19:12:38 +0100 Subject: [PATCH 359/430] Bump Synology DSM to 1.0.1 (#43860) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 1dc0f2a3242..45cad8acfc2 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["python-synology==1.0.0"], + "requirements": ["synologydsm-api==1.0.1"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 08325bc3e7b..92048522278 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1808,9 +1808,6 @@ python-sochain-api==0.0.2 # homeassistant.components.songpal python-songpal==0.12 -# homeassistant.components.synology_dsm -python-synology==1.0.0 - # homeassistant.components.tado python-tado==0.8.1 @@ -2140,6 +2137,9 @@ swisshydrodata==0.0.3 # homeassistant.components.synology_srm synology-srm==0.2.0 +# homeassistant.components.synology_dsm +synologydsm-api==1.0.1 + # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9f3f083b10..b274cc2df35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,9 +889,6 @@ python-openzwave-mqtt==1.3.2 # homeassistant.components.songpal python-songpal==0.12 -# homeassistant.components.synology_dsm -python-synology==1.0.0 - # homeassistant.components.tado python-tado==0.8.1 @@ -1047,6 +1044,9 @@ sunwatcher==0.2.1 # homeassistant.components.surepetcare surepy==0.2.6 +# homeassistant.components.synology_dsm +synologydsm-api==1.0.1 + # homeassistant.components.tellduslive tellduslive==0.10.11 From 5fa00244dad3154523f0232fecc4bedd0bd50d46 Mon Sep 17 00:00:00 2001 From: Wictor Date: Wed, 2 Dec 2020 19:25:35 +0100 Subject: [PATCH 360/430] Allow members of allowed groups to talk to telegram_bot (#43241) --- homeassistant/components/telegram_bot/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index af00c2cb6d0..50905c64fd7 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -744,11 +744,12 @@ class BaseTelegramBotEntity: _LOGGER.error("Incoming message does not have required data (%s)", msg_data) return False, None - if msg_data["from"].get("id") not in self.allowed_chat_ids or ( - "chat" in msg_data + if ( + msg_data["from"].get("id") not in self.allowed_chat_ids and msg_data["chat"].get("id") not in self.allowed_chat_ids ): - # Origin is not allowed. + # Neither from id nor chat id was in allowed_chat_ids, + # origin is not allowed. _LOGGER.error("Incoming message is not allowed (%s)", msg_data) return True, None From 86043b29579eb1a268a39ba46c014f1c92c21c2f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 2 Dec 2020 19:32:42 +0100 Subject: [PATCH 361/430] Add support for system health to Airly integrarion (#43220) --- homeassistant/components/airly/strings.json | 5 ++ .../components/airly/system_health.py | 22 ++++++++ tests/components/airly/test_system_health.py | 50 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 homeassistant/components/airly/system_health.py create mode 100644 tests/components/airly/test_system_health.py diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 3453fb7b38a..afda73ae887 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -19,5 +19,10 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "system_health": { + "info": { + "can_reach_server": "Reach Airly server" + } } } diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py new file mode 100644 index 00000000000..6b683518ebd --- /dev/null +++ b/homeassistant/components/airly/system_health.py @@ -0,0 +1,22 @@ +"""Provide info to system health.""" +from airly import Airly + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + return { + "can_reach_server": system_health.async_check_can_reach_url( + hass, Airly.AIRLY_API_URL + ) + } diff --git a/tests/components/airly/test_system_health.py b/tests/components/airly/test_system_health.py new file mode 100644 index 00000000000..b1f8119e880 --- /dev/null +++ b/tests/components/airly/test_system_health.py @@ -0,0 +1,50 @@ +"""Test Airly system health.""" +import asyncio + +from aiohttp import ClientError + +from homeassistant.components.airly.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.async_mock import Mock +from tests.common import get_system_health_info + + +async def test_airly_system_health(hass, aioclient_mock): + """Test Airly system health.""" + aioclient_mock.get("https://airapi.airly.eu/v2/", text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["0123xyz"] = Mock( + airly=Mock(AIRLY_API_URL="https://airapi.airly.eu/v2/") + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": "ok"} + + +async def test_airly_system_health_fail(hass, aioclient_mock): + """Test Airly system health.""" + aioclient_mock.get("https://airapi.airly.eu/v2/", exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["0123xyz"] = Mock( + airly=Mock(AIRLY_API_URL="https://airapi.airly.eu/v2/") + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}} From b294e1c98c8f4c925d5ef0fa3b72b23df7f09884 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 2 Dec 2020 19:36:16 +0100 Subject: [PATCH 362/430] Add support for system health to AccuWeather integration (#43277) --- .../components/accuweather/strings.json | 6 ++ .../components/accuweather/system_health.py | 27 +++++++++ .../accuweather/test_system_health.py | 58 +++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 homeassistant/components/accuweather/system_health.py create mode 100644 tests/components/accuweather/test_system_health.py diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 7bf61de8476..65aa2a9ed91 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -31,5 +31,11 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach AccuWeather server", + "remaining_requests": "Remaining allowed requests" + } } } diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py new file mode 100644 index 00000000000..58c9ba35881 --- /dev/null +++ b/homeassistant/components/accuweather/system_health.py @@ -0,0 +1,27 @@ +"""Provide info to system health.""" +from accuweather.const import ENDPOINT + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +from .const import COORDINATOR, DOMAIN + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + remaining_requests = list(hass.data[DOMAIN].values())[0][ + COORDINATOR + ].accuweather.requests_remaining + + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT), + "remaining_requests": remaining_requests, + } diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py new file mode 100644 index 00000000000..cf8c931e123 --- /dev/null +++ b/tests/components/accuweather/test_system_health.py @@ -0,0 +1,58 @@ +"""Test AccuWeather system health.""" +import asyncio + +from aiohttp import ClientError + +from homeassistant.components.accuweather.const import COORDINATOR, DOMAIN +from homeassistant.setup import async_setup_component + +from tests.async_mock import Mock +from tests.common import get_system_health_info + + +async def test_accuweather_system_health(hass, aioclient_mock): + """Test AccuWeather system health.""" + aioclient_mock.get("https://dataservice.accuweather.com/", text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["0123xyz"] = {} + hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock( + accuweather=Mock(requests_remaining="42") + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == { + "can_reach_server": "ok", + "remaining_requests": "42", + } + + +async def test_accuweather_system_health_fail(hass, aioclient_mock): + """Test AccuWeather system health.""" + aioclient_mock.get("https://dataservice.accuweather.com/", exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["0123xyz"] = {} + hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock( + accuweather=Mock(requests_remaining="0") + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == { + "can_reach_server": {"type": "failed", "error": "unreachable"}, + "remaining_requests": "0", + } From 39601090bafd1afeb8b360ab42d71cd2a91b2910 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 2 Dec 2020 10:40:49 -0800 Subject: [PATCH 363/430] Automatically select "Solid" effect in Hyperion (#43799) --- homeassistant/components/hyperion/light.py | 9 +++-- tests/components/hyperion/test_light.py | 46 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 90e362b3b16..b8e9040f7ce 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -342,7 +342,7 @@ class HyperionLight(LightEntity): @property def is_on(self) -> bool: """Return true if not black.""" - return bool(self._client.is_on()) + return bool(self._client.is_on()) and self._client.visible_priority is not None @property def icon(self) -> str: @@ -413,7 +413,10 @@ class HyperionLight(LightEntity): # == Get key parameters == brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) - effect = kwargs.get(ATTR_EFFECT, self._effect) + if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs: + effect = KEY_EFFECT_SOLID + else: + effect = kwargs.get(ATTR_EFFECT, self._effect) rgb_color: Sequence[int] if ATTR_HS_COLOR in kwargs: rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) @@ -549,7 +552,7 @@ class HyperionLight(LightEntity): rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB], effect=KEY_EFFECT_SOLID, ) - self.async_write_ha_state() + self.async_write_ha_state() def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion effects.""" diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 36b3684f736..5366f6e14d1 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -502,6 +502,36 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT assert entity_state.attributes["effect"] == effect + # On (=), 100% (=), [0,0,255] (!) + # Ensure changing the color will move the effect to 'Solid' automatically. + hs_color = (240.0, 100.0) + client.async_send_set_color = AsyncMock(return_value=True) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_HS_COLOR: hs_color}, + blocking=True, + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: (0, 0, 255), + const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + } + ) + # Simulate a state callback from Hyperion. + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)}, + } + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + assert entity_state.attributes["hs_color"] == hs_color + assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB + assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID + # No calls if disconnected. client.has_loaded_state = False _call_registered_callback(client, "client-update", {"loaded-state": False}) @@ -627,6 +657,14 @@ async def test_light_async_updates_from_hyperion_client( assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB assert entity_state.attributes["hs_color"] == (180.0, 100.0) + # Update priorities (None) + client.visible_priority = None + + _call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + # Update effect list effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] client.effects = effects @@ -648,6 +686,10 @@ async def test_light_async_updates_from_hyperion_client( # Update connection status (e.g. re-connection) client.has_loaded_state = True + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: rgb}, + } _call_registered_callback(client, "client-update", {"loaded-state": True}) entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state @@ -691,7 +733,7 @@ async def test_unload_entry(hass: HomeAssistantType) -> None: assert client.async_client_disconnect.call_count == 2 -async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: +async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] """Test warning on old version.""" client = create_mock_client() client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.7") @@ -700,7 +742,7 @@ async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: assert "Please consider upgrading" in caplog.text -async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: +async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] """Test no warning on acceptable version.""" client = create_mock_client() client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.9") From 8efa9c5097409d160834fa67a80edbc87044b5c5 Mon Sep 17 00:00:00 2001 From: Kiall Mac Innes Date: Wed, 2 Dec 2020 18:42:28 +0000 Subject: [PATCH 364/430] Add support for MQTT Scenes (#42639) --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/scene.py | 144 ++++++++++++++++ tests/components/mqtt/test_scene.py | 181 +++++++++++++++++++++ 4 files changed, 327 insertions(+) create mode 100644 homeassistant/components/mqtt/scene.py create mode 100644 tests/components/mqtt/test_scene.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 05d7d0adb86..73caf023ef6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -145,6 +145,7 @@ PLATFORMS = [ "fan", "light", "lock", + "scene", "sensor", "switch", "vacuum", diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 7f9a6730285..d1e64d44bbc 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -37,6 +37,7 @@ SUPPORTED_COMPONENTS = [ "fan", "light", "lock", + "scene", "sensor", "switch", "tag", diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py new file mode 100644 index 00000000000..4f4380332fd --- /dev/null +++ b/homeassistant/components/mqtt/scene.py @@ -0,0 +1,144 @@ +"""Support for MQTT scenes.""" +import logging + +import voluptuous as vol + +from homeassistant.components import mqtt, scene +from homeassistant.components.scene import Scene +from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from . import ( + ATTR_DISCOVERY_HASH, + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + DOMAIN, + PLATFORMS, + MqttAvailability, + MqttDiscoveryUpdate, +) +from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "MQTT Scene" +DEFAULT_RETAIN = False + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_ON): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + } +).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) + + +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT scene through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_entity(config, async_add_entities) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT scene dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add a MQTT scene.""" + discovery_data = discovery_payload.discovery_data + try: + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity( + config, async_add_entities, config_entry, discovery_data + ) + except Exception: + clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(scene.DOMAIN, "mqtt"), async_discover + ) + + +async def _async_setup_entity( + config, async_add_entities, config_entry=None, discovery_data=None +): + """Set up the MQTT scene.""" + async_add_entities([MqttScene(config, config_entry, discovery_data)]) + + +class MqttScene( + MqttAvailability, + MqttDiscoveryUpdate, + Scene, +): + """Representation of a scene that can be activated using MQTT.""" + + def __init__(self, config, config_entry, discovery_data): + """Initialize the MQTT scene.""" + self._state = False + self._sub_state = None + + self._unique_id = config.get(CONF_UNIQUE_ID) + + # Load config + self._setup_from_config(config) + + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + + async def async_added_to_hass(self): + """Subscribe to MQTT events.""" + await super().async_added_to_hass() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA(discovery_payload) + self._setup_from_config(config) + await self.availability_discovery_update(config) + self.async_write_ha_state() + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._config = config + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + await MqttAvailability.async_will_remove_from_hass(self) + await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + + @property + def name(self): + """Return the name of the scene.""" + return self._config[CONF_NAME] + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def icon(self): + """Return the icon.""" + return self._config.get(CONF_ICON) + + async def async_activate(self, **kwargs): + """Activate the scene. + + This method is a coroutine. + """ + mqtt.async_publish( + self.hass, + self._config[CONF_COMMAND_TOPIC], + self._config[CONF_PAYLOAD_ON], + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py new file mode 100644 index 00000000000..0e3341bd15f --- /dev/null +++ b/tests/components/mqtt/test_scene.py @@ -0,0 +1,181 @@ +"""The tests for the MQTT scene platform.""" +import copy +import json + +import pytest + +from homeassistant.components import scene +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_unchanged, + help_test_unique_id, +) + +from tests.async_mock import patch + +DEFAULT_CONFIG = { + scene.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test-topic", + "payload_on": "test-payload-on", + } +} + + +async def test_sending_mqtt_commands(hass, mqtt_mock): + """Test the sending MQTT commands.""" + fake_state = ha.State("scene.test", scene.STATE) + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + scene.DOMAIN, + { + scene.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + }, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("scene.test") + assert state.state == scene.STATE + + data = {ATTR_ENTITY_ID: "scene.test"} + await hass.services.async_call(scene.DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "beer on", 0, False + ) + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + config = { + scene.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_on": 1, + } + } + + await help_test_default_availability_payload( + hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + config = { + scene.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_on": 1, + } + } + + await help_test_custom_availability_payload( + hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one scene per unique_id.""" + config = { + scene.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, scene.DOMAIN, config) + + +async def test_discovery_removal_scene(hass, mqtt_mock, caplog): + """Test removal of discovered scene.""" + data = '{ "name": "test",' ' "command_topic": "test_topic" }' + await help_test_discovery_removal(hass, mqtt_mock, caplog, scene.DOMAIN, data) + + +async def test_discovery_update_payload(hass, mqtt_mock, caplog): + """Test update of discovered scene.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["payload_on"] = "ON" + config2["payload_on"] = "ACTIVATE" + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + scene.DOMAIN, + data1, + data2, + ) + + +async def test_discovery_update_unchanged_scene(hass, mqtt_mock, caplog): + """Test update of discovered scene.""" + data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' + with patch( + "homeassistant.components.mqtt.scene.MqttScene.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, scene.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' + await help_test_discovery_broken( + hass, mqtt_mock, caplog, scene.DOMAIN, data1, data2 + ) From 9043b7b214256f82ec526176ed38baca1a3ada52 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 2 Dec 2020 20:03:29 +0100 Subject: [PATCH 365/430] Add ozw add-on discovery and mqtt client (#43838) --- homeassistant/components/hassio/__init__.py | 13 ++ homeassistant/components/ozw/__init__.py | 82 +++++++++-- homeassistant/components/ozw/config_flow.py | 34 ++++- homeassistant/components/ozw/const.py | 2 +- homeassistant/components/ozw/manifest.json | 4 +- homeassistant/components/ozw/strings.json | 12 +- .../components/ozw/translations/en.json | 5 + homeassistant/components/ozw/websocket_api.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ozw/conftest.py | 9 ++ tests/components/ozw/test_config_flow.py | 129 +++++++++++++++++- tests/components/ozw/test_init.py | 85 ++++++++++++ 13 files changed, 353 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 1e05321b60f..e8b874b2334 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging import os +from typing import Optional import voluptuous as vol @@ -23,6 +24,7 @@ from homeassistant.util.dt import utcnow from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view +from .const import ATTR_DISCOVERY from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView @@ -200,6 +202,17 @@ async def async_set_addon_options( return await hassio.send_command(command, payload=options) +@bind_hass +async def async_get_addon_discovery_info( + hass: HomeAssistantType, slug: str +) -> Optional[dict]: + """Return discovery data for an add-on.""" + hassio = hass.data[DOMAIN] + data = await hassio.retrieve_discovery_messages() + discovered_addons = data[ATTR_DISCOVERY] + return next((addon for addon in discovered_addons if addon["addon"] == slug), None) + + @callback @bind_hass def get_info(hass): diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 2fe45018182..c0d50e18abc 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -17,22 +17,25 @@ from openzwavemqtt.const import ( ) from openzwavemqtt.models.node import OZWNode from openzwavemqtt.models.value import OZWValue +from openzwavemqtt.util.mqtt_client import MQTTClient import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.dispatcher import async_dispatcher_send from . import const from .const import ( CONF_INTEGRATION_CREATED_ADDON, + CONF_USE_ADDON, DATA_UNSUBSCRIBE, DOMAIN, MANAGER, - OPTIONS, PLATFORMS, TOPIC_OPENZWAVE, ) @@ -50,13 +53,11 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) DATA_DEVICES = "zwave-mqtt-devices" +DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" async def async_setup(hass: HomeAssistant, config: dict): """Initialize basic config of ozw component.""" - if "mqtt" not in hass.config.components: - _LOGGER.error("MQTT integration is not set up") - return False hass.data[DOMAIN] = {} return True @@ -69,16 +70,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): data_nodes = {} data_values = {} removed_nodes = [] + manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"} - @callback - def send_message(topic, payload): - mqtt.async_publish(hass, topic, json.dumps(payload)) + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) - options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") + if entry.data.get(CONF_USE_ADDON): + # Do not use MQTT integration. Use own MQTT client. + # Retrieve discovery info from the OpenZWave add-on. + discovery_info = await hass.components.hassio.async_get_addon_discovery_info( + "core_zwave" + ) + + if not discovery_info: + _LOGGER.error("Failed to get add-on discovery info") + raise ConfigEntryNotReady + + discovery_info_config = discovery_info["config"] + + host = discovery_info_config["host"] + port = discovery_info_config["port"] + username = discovery_info_config["username"] + password = discovery_info_config["password"] + mqtt_client = MQTTClient(host, port, username=username, password=password) + manager_options["send_message"] = mqtt_client.send_message + + else: + if "mqtt" not in hass.config.components: + _LOGGER.error("MQTT integration is not set up") + return False + + @callback + def send_message(topic, payload): + mqtt.async_publish(hass, topic, json.dumps(payload)) + + manager_options["send_message"] = send_message + + options = OZWOptions(**manager_options) manager = OZWManager(options) hass.data[DOMAIN][MANAGER] = manager - hass.data[DOMAIN][OPTIONS] = options @callback def async_node_added(node): @@ -234,11 +265,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): for component in PLATFORMS ] ) - ozw_data[DATA_UNSUBSCRIBE].append( - await mqtt.async_subscribe( - hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message + if entry.data.get(CONF_USE_ADDON): + mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager)) + + async def async_stop_mqtt_client(event=None): + """Stop the mqtt client. + + Do not unsubscribe the manager topic. + """ + mqtt_client_task.cancel() + try: + await mqtt_client_task + except asyncio.CancelledError: + pass + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client) + ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client + + else: + ozw_data[DATA_UNSUBSCRIBE].append( + await mqtt.async_subscribe( + hass, f"{manager.options.topic_prefix}#", async_receive_message + ) ) - ) hass.async_create_task(start_platforms()) @@ -262,6 +311,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): # unsubscribe all listeners for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: unsubscribe_listener() + + if entry.data.get(CONF_USE_ADDON): + async_stop_mqtt_client = hass.data[DOMAIN][entry.entry_id][ + DATA_STOP_MQTT_CLIENT + ] + await async_stop_mqtt_client() + hass.data[DOMAIN].pop(entry.entry_id) return True diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 9ecbcbe76bb..4543bc27984 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow -from .const import CONF_INTEGRATION_CREATED_ADDON +from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -16,7 +16,6 @@ CONF_ADDON_DEVICE = "device" CONF_ADDON_NETWORK_KEY = "network_key" CONF_NETWORK_KEY = "network_key" CONF_USB_PATH = "usb_path" -CONF_USE_ADDON = "use_addon" TITLE = "OpenZWave" ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool}) @@ -43,17 +42,36 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - # Currently all flow results need the MQTT integration. - # This will change when we have the direct MQTT client connection. - # When that is implemented, move this check to _async_use_mqtt_integration. - if "mqtt" not in self.hass.config.components: - return self.async_abort(reason="mqtt_required") + # Set a unique_id to make sure discovery flow is aborted on progress. + await self.async_set_unique_id(DOMAIN, raise_on_progress=False) if not self.hass.components.hassio.is_hassio(): return self._async_use_mqtt_integration() return await self.async_step_on_supervisor() + async def async_step_hassio(self, discovery_info): + """Receive configuration from add-on discovery info. + + This flow is triggered by the OpenZWave add-on. + """ + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm the add-on discovery.""" + if user_input is not None: + self.use_addon = True + return self._async_create_entry_from_vars() + + return self.async_show_form(step_id="hassio_confirm") + def _async_create_entry_from_vars(self): """Return a config entry for the flow.""" return self.async_create_entry( @@ -73,6 +91,8 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): This is the entry point for the logic that is needed when this integration will depend on the MQTT integration. """ + if "mqtt" not in self.hass.config.components: + return self.async_abort(reason="mqtt_required") return self._async_create_entry_from_vars() async def async_step_on_supervisor(self, user_input=None): diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 160b251eeca..f8d5090aa84 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -12,6 +12,7 @@ DOMAIN = "ozw" DATA_UNSUBSCRIBE = "unsubscribe" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_USE_ADDON = "use_addon" PLATFORMS = [ BINARY_SENSOR_DOMAIN, @@ -24,7 +25,6 @@ PLATFORMS = [ SWITCH_DOMAIN, ] MANAGER = "manager" -OPTIONS = "options" # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index fa25f984076..a1409fd79a8 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", "requirements": [ - "python-openzwave-mqtt==1.3.2" + "python-openzwave-mqtt[mqtt-client]==1.4.0" ], "after_dependencies": [ "mqtt" @@ -14,4 +14,4 @@ "@marcelveldt", "@MartinHjelmare" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json index f006f0663f0..ed9816c57f2 100644 --- a/homeassistant/components/ozw/strings.json +++ b/homeassistant/components/ozw/strings.json @@ -4,17 +4,25 @@ "on_supervisor": { "title": "Select connection method", "description": "Do you want to use the OpenZWave Supervisor add-on?", - "data": {"use_addon": "Use the OpenZWave Supervisor add-on"} + "data": { "use_addon": "Use the OpenZWave Supervisor add-on" } }, "install_addon": { "title": "The OpenZWave add-on installation has started" }, "start_addon": { "title": "Enter the OpenZWave add-on configuration", - "data": {"usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key"} + "data": { + "usb_path": "[%key:common::config_flow::data::usb_path%]", + "network_key": "Network Key" + } + }, + "hassio_confirm": { + "title": "Set up OpenZWave integration with the OpenZWave add-on" } }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "addon_info_failed": "Failed to get OpenZWave add-on info.", "addon_install_failed": "Failed to install the OpenZWave add-on.", "addon_set_config_failed": "Failed to set OpenZWave configuration.", diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json index e21b1819883..1c837e0bde5 100644 --- a/homeassistant/components/ozw/translations/en.json +++ b/homeassistant/components/ozw/translations/en.json @@ -4,6 +4,8 @@ "addon_info_failed": "Failed to get OpenZWave add-on info.", "addon_install_failed": "Failed to install the OpenZWave add-on.", "addon_set_config_failed": "Failed to set OpenZWave configuration.", + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "mqtt_required": "The MQTT integration is not set up", "single_instance_allowed": "Already configured. Only a single configuration possible." }, @@ -14,6 +16,9 @@ "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." }, "step": { + "hassio_confirm": { + "title": "Set up OpenZWave integration with the OpenZWave add-on" + }, "install_addon": { "title": "The OpenZWave add-on installation has started" }, diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index ea6df900907..3ee6e040743 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -21,7 +21,7 @@ from homeassistant.components import websocket_api from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER, OPTIONS +from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER from .lock import ATTR_USERCODE TYPE = "type" @@ -461,7 +461,7 @@ def websocket_refresh_node_info(hass, connection, msg): """Tell OpenZWave to re-interview a node.""" manager = hass.data[DOMAIN][MANAGER] - options = hass.data[DOMAIN][OPTIONS] + options = manager.options @callback def forward_node(node): diff --git a/requirements_all.txt b/requirements_all.txt index 92048522278..0ec2442425e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1794,7 +1794,7 @@ python-nest==4.1.0 python-nmap==0.6.1 # homeassistant.components.ozw -python-openzwave-mqtt==1.3.2 +python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b274cc2df35..09a722d716d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ python-miio==0.5.4 python-nest==4.1.0 # homeassistant.components.ozw -python-openzwave-mqtt==1.3.2 +python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.songpal python-songpal==0.12 diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index f2518fb8007..d3f8288658c 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -253,3 +253,12 @@ def mock_uninstall_addon(): "homeassistant.components.hassio.async_uninstall_addon" ) as uninstall_addon: yield uninstall_addon + + +@pytest.fixture(name="get_addon_discovery_info") +def mock_get_addon_discovery_info(): + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.hassio.async_get_addon_discovery_info" + ) as get_addon_discovery_info: + yield get_addon_discovery_info diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 7a2b89967c5..289b6c7f4cd 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -9,6 +9,14 @@ from homeassistant.components.ozw.const import DOMAIN from tests.async_mock import patch from tests.common import MockConfigEntry +ADDON_DISCOVERY_INFO = { + "addon": "OpenZWave", + "host": "host1", + "port": 1234, + "username": "name1", + "password": "pass1", +} + @pytest.fixture(name="supervisor") def mock_supervisor_fixture(): @@ -44,7 +52,7 @@ def mock_addon_installed(addon_info): def mock_addon_options(addon_info): """Mock add-on options.""" addon_info.return_value["options"] = {} - return addon_info + return addon_info.return_value["options"] @pytest.fixture(name="set_addon_options") @@ -361,3 +369,122 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ assert result["type"] == "abort" assert result["reason"] == "addon_install_failed" + + +async def test_supervisor_discovery(hass, supervisor, addon_running, addon_options): + """Test flow started from Supervisor discovery.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_clean_discovery_on_user_create( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is cleaned up when a user flow is finished.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": False} + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress()) == 0 + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_discovery_with_user_flow( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is aborted if a user flow is in progress.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" + assert len(hass.config_entries.flow.async_progress()) == 1 + + +async def test_abort_discovery_with_existing_entry( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is aborted if an entry already exists.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=DOMAIN) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index bf1eefe866a..efc38fa63c2 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -5,6 +5,7 @@ from homeassistant.components.ozw import DOMAIN, PLATFORMS, const from .common import setup_ozw +from tests.async_mock import patch from tests.common import MockConfigEntry @@ -23,6 +24,18 @@ async def test_init_entry(hass, generic_data): assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) +async def test_setup_entry_without_mqtt(hass): + """Test setting up config entry without mqtt integration setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + ) + entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry.entry_id) + + async def test_unload_entry(hass, generic_data, switch_msg, caplog): """Test unload the config entry.""" entry = MockConfigEntry( @@ -128,3 +141,75 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the OpenZWave add-on" in caplog.text + + +async def test_setup_entry_with_addon(hass, get_addon_discovery_info): + """Test set up entry using OpenZWave add-on.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_client.return_value.start_client.call_count == 1 + + # Verify integration + platform loaded. + assert "ozw" in hass.config.components + for platform in PLATFORMS: + assert platform in hass.config.components, platform + assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" + + # Verify services registered + assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) + assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) + + +async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): + """Test set up entry using OpenZWave add-on but missing discovery info.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True}, + ) + entry.add_to_hass(hass) + + get_addon_discovery_info.return_value = None + + with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: + assert not await hass.config_entries.async_setup(entry.entry_id) + + assert mock_client.return_value.start_client.call_count == 0 + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry_with_addon( + hass, get_addon_discovery_info, generic_data, switch_msg, caplog +): + """Test unload the config entry using the OpenZWave add-on.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True}, + ) + entry.add_to_hass(hass) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_client.return_value.start_client.call_count == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED From a4b1990487cc926695303b4c75a8821bd77c19ba Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Wed, 2 Dec 2020 22:44:02 +0300 Subject: [PATCH 366/430] Add telegram_bot.send_voice service (#43433) Co-authored-by: Franck Nijhof --- homeassistant/components/telegram/notify.py | 10 ++++- .../components/telegram_bot/__init__.py | 4 ++ .../components/telegram_bot/services.yaml | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index c0f3f624af9..fd7e731194c 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -23,6 +23,7 @@ ATTR_KEYBOARD = "keyboard" ATTR_INLINE_KEYBOARD = "inline_keyboard" ATTR_PHOTO = "photo" ATTR_VIDEO = "video" +ATTR_VOICE = "voice" ATTR_DOCUMENT = "document" CONF_CHAT_ID = "chat_id" @@ -65,7 +66,7 @@ class TelegramNotificationService(BaseNotificationService): keys = keys if isinstance(keys, list) else [keys] service_data.update(inline_keyboard=keys) - # Send a photo, video, document, or location + # Send a photo, video, document, voice, or location if data is not None and ATTR_PHOTO in data: photos = data.get(ATTR_PHOTO) photos = photos if isinstance(photos, list) else [photos] @@ -80,6 +81,13 @@ class TelegramNotificationService(BaseNotificationService): service_data.update(video_data) self.hass.services.call(DOMAIN, "send_video", service_data=service_data) return + if data is not None and ATTR_VOICE in data: + voices = data.get(ATTR_VOICE) + voices = voices if isinstance(voices, list) else [voices] + for voice_data in voices: + service_data.update(voice_data) + self.hass.services.call(DOMAIN, "send_voice", service_data=service_data) + return if data is not None and ATTR_LOCATION in data: service_data.update(data.get(ATTR_LOCATION)) return self.hass.services.call( diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 50905c64fd7..fc592c9e5c0 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -80,6 +80,7 @@ SERVICE_SEND_MESSAGE = "send_message" SERVICE_SEND_PHOTO = "send_photo" SERVICE_SEND_STICKER = "send_sticker" SERVICE_SEND_VIDEO = "send_video" +SERVICE_SEND_VOICE = "send_voice" SERVICE_SEND_DOCUMENT = "send_document" SERVICE_SEND_LOCATION = "send_location" SERVICE_EDIT_MESSAGE = "edit_message" @@ -224,6 +225,7 @@ SERVICE_MAP = { SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION, SERVICE_EDIT_MESSAGE: SERVICE_SCHEMA_EDIT_MESSAGE, @@ -366,6 +368,7 @@ async def async_setup(hass, config): SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, SERVICE_SEND_VIDEO, + SERVICE_SEND_VOICE, SERVICE_SEND_DOCUMENT, ]: await hass.async_add_executor_job( @@ -672,6 +675,7 @@ class TelegramNotificationService: SERVICE_SEND_PHOTO: self.bot.sendPhoto, SERVICE_SEND_STICKER: self.bot.sendSticker, SERVICE_SEND_VIDEO: self.bot.sendVideo, + SERVICE_SEND_VOICE: self.bot.sendVoice, SERVICE_SEND_DOCUMENT: self.bot.sendDocument, }.get(file_type) file_content = load_data( diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 8a66d2cab3a..a4e0adc81a8 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -151,6 +151,46 @@ send_video: description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" +send_voice: + description: Send a voice message. + fields: + url: + description: Remote path to a voice message. + example: "http://example.org/path/to/the/voice.opus" + file: + description: Local path to a voice message. + example: "/path/to/the/voice.opus" + caption: + description: The title of the voice message. + example: "My microphone recording" + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: "[12345, 67890] or 12345" + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + verify_ssl: + description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. + example: false + timeout: + description: Timeout for send voice. Will help with timeout errors (poor internet connection, etc) + example: "1000" + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + message_tag: + description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + example: "msg_to_edit" + send_document: description: Send a document. fields: From f2a371257da9e5a305dbc7a5e3156d7d71ac3843 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Dec 2020 20:51:05 +0100 Subject: [PATCH 367/430] Translation update --- .../components/abode/translations/lb.json | 3 +- .../accuweather/translations/en.json | 6 + .../accuweather/translations/et.json | 6 + .../components/airly/translations/en.json | 5 + .../components/airly/translations/et.json | 5 + .../components/apple_tv/translations/en.json | 120 +++++++++--------- .../components/apple_tv/translations/et.json | 64 ++++++++++ .../components/aurora/translations/lb.json | 3 +- .../components/bsblan/translations/lb.json | 4 +- .../components/bsblan/translations/pl.json | 4 +- .../cloudflare/translations/lb.json | 10 +- .../components/deconz/translations/lb.json | 1 + .../device_tracker/translations/zh-Hans.json | 4 + .../components/dexcom/translations/lb.json | 1 + .../components/dsmr/translations/lb.json | 10 ++ .../components/hassio/translations/lb.json | 1 + .../components/hyperion/translations/lb.json | 52 ++++++++ .../components/hyperion/translations/pl.json | 52 ++++++++ .../components/ipma/translations/pl.json | 5 + .../components/local_ip/translations/lb.json | 1 + .../logi_circle/translations/et.json | 2 +- .../mobile_app/translations/cs.json | 5 + .../mobile_app/translations/es.json | 5 + .../mobile_app/translations/et.json | 5 + .../mobile_app/translations/lb.json | 5 + .../mobile_app/translations/no.json | 5 + .../mobile_app/translations/pl.json | 5 + .../mobile_app/translations/zh-Hant.json | 5 + .../motion_blinds/translations/lb.json | 3 +- .../components/nest/translations/cs.json | 5 + .../components/nest/translations/pl.json | 8 ++ .../components/ozw/translations/et.json | 5 + .../components/ozw/translations/pl.json | 6 + .../person/translations/zh-Hans.json | 2 +- .../components/vizio/translations/lb.json | 1 + 35 files changed, 356 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/apple_tv/translations/et.json create mode 100644 homeassistant/components/hyperion/translations/lb.json create mode 100644 homeassistant/components/hyperion/translations/pl.json diff --git a/homeassistant/components/abode/translations/lb.json b/homeassistant/components/abode/translations/lb.json index 127281176be..2058d3353c9 100644 --- a/homeassistant/components/abode/translations/lb.json +++ b/homeassistant/components/abode/translations/lb.json @@ -20,7 +20,8 @@ "data": { "password": "Passwuert", "username": "E-Mail" - } + }, + "title": "F\u00ebll deng Abode Login Informatiounen aus" }, "user": { "data": { diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index b9986eda30f..b737c420a2d 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -31,5 +31,11 @@ "title": "AccuWeather Options" } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach AccuWeather server", + "remaining_requests": "Remaining allowed requests" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index 1b0bdbe643a..ebbceb69b0a 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -31,5 +31,11 @@ "title": "AccuWeatheri valikud" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendu Accuweatheri serveriga", + "remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index 4aa35c3cada..720f68f8349 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach Airly server" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index 270c97004f8..0d46a0f7643 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -19,5 +19,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendu Airly serveriga" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index 8507c84af3b..0fe914c3d86 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -1,64 +1,64 @@ { - "title": "Apple TV", - "config": { - "flow_title": "Apple TV: {name}", - "step": { - "user": { - "title": "Setup a new Apple TV", - "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", - "data": { - "device_input": "Device" + "config": { + "abort": { + "already_configured_device": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "no_devices_found": "No devices found on the network", + "unknown": "Unexpected error" + }, + "error": { + "already_configured": "Device is already configured", + "invalid_auth": "Invalid authentication", + "no_devices_found": "No devices found on the network", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", + "unknown": "Unexpected error" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!", + "title": "Confirm adding Apple TV" + }, + "pair_no_pin": { + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue.", + "title": "Pairing" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Code" + }, + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "title": "Pairing" + }, + "reconfigure": { + "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured.", + "title": "Device reconfiguration" + }, + "service_problem": { + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored.", + "title": "Failed to add service" + }, + "user": { + "data": { + "device_input": "Device" + }, + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", + "title": "Setup a new Apple TV" + } } - }, - "reconfigure": { - "title": "Device reconfiguration", - "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured." - }, - "pair_with_pin": { - "title": "Pairing", - "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", - "data": { - "pin": "PIN Code" - } - }, - "pair_no_pin": { - "title": "Pairing", - "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue." - }, - "service_problem": { - "title": "Failed to add service", - "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." - }, - "confirm": { - "title": "Confirm adding Apple TV", - "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!" - } }, - "error": { - "no_devices_found": "No devices found on the network", - "already_configured": "Device is already configured", - "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", - "unknown": "Unexpected error", - "invalid_auth": "Invalid authentication" - }, - "abort": { - "no_devices_found": "No devices found on the network", - "already_configured_device": "Device is already configured", - "device_did_not_pair": "No attempt to finish pairing process was made from the device.", - "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", - "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", - "already_in_progress": "Configuration flow is already in progress", - "unknown": "Unexpected error" - } - }, - "options": { - "step": { - "init": { - "description": "Configure general device settings", - "data": { - "start_off": "Do not turn device on when starting Home Assistant" + "options": { + "step": { + "init": { + "data": { + "start_off": "Do not turn device on when starting Home Assistant" + }, + "description": "Configure general device settings" + } } - } - } - } -} + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/et.json b/homeassistant/components/apple_tv/translations/et.json new file mode 100644 index 00000000000..597c4756907 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/et.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "backoff": "Seade ei aktsepteeri praegu sidumisn\u00f5udeid (v\u00f5ib-olla oled liiga palju kordi vale PIN-koodi sisestanud), proovi hiljem uuesti.", + "device_did_not_pair": "Seade ei \u00fcritatud sidumisprotsessi l\u00f5pule viia.", + "invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "invalid_auth": "Vigane autentimine", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Oled Home Assistantile lisamas Apple TV-d nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!", + "title": "Kinnita Apple TV lisamine" + }, + "pair_no_pin": { + "description": "Teenuse {protocol} sidumine on vajalik. J\u00e4tkamiseks sisesta oma Apple TV-s PIN-kood {pin} .", + "title": "Sidumine" + }, + "pair_with_pin": { + "data": { + "pin": "PIN kood" + }, + "description": "Vajalik on protokolli {protocol} sidumine. Sisesta ekraanil kuvatav PIN-kood. Alguse nullid j\u00e4etakse v\u00e4lja, st. sisesta 123, kui kuvatav kood on 0123.", + "title": "Sidumine" + }, + "reconfigure": { + "description": "Sellel Apple TV-l on \u00fchendusprobleemid ja see tuleb uuesti seadistada.", + "title": "Seadme \u00fcmberseadistamine" + }, + "service_problem": { + "description": "Protokolli {protocol} sidumisel ilmnes probleem. Seda ignoreeritakse.", + "title": "Teenuse lisamine eba\u00f5nnestus." + }, + "user": { + "data": { + "device_input": "Seade" + }, + "description": "Alustuseks sisesta lisatava Apple TV seadme nimi (nt K\u00f6\u00f6k v\u00f5i Magamistuba) v\u00f5i IP-aadress. Kui m\u00f5ni seade leiti teie v\u00f5rgust automaatselt kuvatakse see allpool. \n\n Kui ei n\u00e4e oma seadet v\u00f5i on probleeme, proovi m\u00e4\u00e4rata seadme IP-aadress. \n\n {devices}", + "title": "Seadista uus Apple TV sidumine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u00c4ra l\u00fclita seadet Home Assistanti k\u00e4ivitamisel sisse" + }, + "description": "Seadme \u00fclds\u00e4tete seadistamine" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/lb.json b/homeassistant/components/aurora/translations/lb.json index 2fb42afcab3..e03a50e0183 100644 --- a/homeassistant/components/aurora/translations/lb.json +++ b/homeassistant/components/aurora/translations/lb.json @@ -21,5 +21,6 @@ } } } - } + }, + "title": "NOAA Aurora Sensor" } \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/lb.json b/homeassistant/components/bsblan/translations/lb.json index 56ace699cee..3f8da84cea0 100644 --- a/homeassistant/components/bsblan/translations/lb.json +++ b/homeassistant/components/bsblan/translations/lb.json @@ -12,7 +12,9 @@ "data": { "host": "Host", "passkey": "Passkey Zeechefolleg", - "port": "Port" + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" }, "description": "BSB-Lan Apparat ariichten fir d'Integratioun mam Home Assistant.", "title": "Mam BSB-Lan Apparat verbannen" diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json index 5ab1eabbe26..5cf79db3fba 100644 --- a/homeassistant/components/bsblan/translations/pl.json +++ b/homeassistant/components/bsblan/translations/pl.json @@ -12,7 +12,9 @@ "data": { "host": "Nazwa hosta lub adres IP", "passkey": "Ci\u0105g klucza dost\u0119pu", - "port": "Port" + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" }, "description": "Konfiguracja urz\u0105dzenia BSB-LAN w celu integracji z Home Assistantem.", "title": "Po\u0142\u0105czenie z urz\u0105dzeniem BSB-Lan" diff --git a/homeassistant/components/cloudflare/translations/lb.json b/homeassistant/components/cloudflare/translations/lb.json index 753406d8d92..c32e7e7603c 100644 --- a/homeassistant/components/cloudflare/translations/lb.json +++ b/homeassistant/components/cloudflare/translations/lb.json @@ -11,16 +11,24 @@ }, "flow_title": "Cloudflare: {name}", "step": { + "records": { + "data": { + "records": "Enregistrement" + }, + "title": "Wiel d'Enregistrments aus fir ze ver\u00e4nneren" + }, "user": { "data": { "api_token": "API Jeton" }, + "description": "D\u00ebs Integratioun ben\u00e9idget een API Jeton dee mat Zone:Zone:Read a Zone:DNS:Edit Rechter fir all Zone an dengem Kont erstallt gouf.", "title": "Mat Cloudflare verbannen" }, "zone": { "data": { "zone": "Zon" - } + }, + "title": "Wiel d'Zone aus d\u00e9i aktualis\u00e9iert soll ginn." } } } diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json index 25adae28583..bb556842194 100644 --- a/homeassistant/components/deconz/translations/lb.json +++ b/homeassistant/components/deconz/translations/lb.json @@ -66,6 +66,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_rotated_fast": "Kn\u00e4ppche schnell 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", diff --git a/homeassistant/components/device_tracker/translations/zh-Hans.json b/homeassistant/components/device_tracker/translations/zh-Hans.json index 28adcdbdd1a..c019a3dcda8 100644 --- a/homeassistant/components/device_tracker/translations/zh-Hans.json +++ b/homeassistant/components/device_tracker/translations/zh-Hans.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} \u5728\u5bb6", "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" + }, + "trigger_type": { + "enters": "{entity_name} \u8fdb\u5165\u533a\u57df", + "leaves": "{entity_name} \u79bb\u5f00\u533a\u57df" } }, "state": { diff --git a/homeassistant/components/dexcom/translations/lb.json b/homeassistant/components/dexcom/translations/lb.json index 86aced8ee11..7bc32ef7e5e 100644 --- a/homeassistant/components/dexcom/translations/lb.json +++ b/homeassistant/components/dexcom/translations/lb.json @@ -15,6 +15,7 @@ "server": "Server", "username": "Benotzernumm" }, + "description": "F\u00ebll deng Desxcom Share Umeldungs Informatiounen aus", "title": "Dexcom Integration ariichten" } } diff --git a/homeassistant/components/dsmr/translations/lb.json b/homeassistant/components/dsmr/translations/lb.json index 6469543442e..b742d16b36d 100644 --- a/homeassistant/components/dsmr/translations/lb.json +++ b/homeassistant/components/dsmr/translations/lb.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Apparat ass scho konfigur\u00e9iert" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minimum Z\u00e4it zw\u00ebschen Entit\u00e9it's Aktualis\u00e9ierungen [s]" + }, + "title": "DSMR Optiounen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lb.json b/homeassistant/components/hassio/translations/lb.json index eefcb6110a1..54aae5a2c59 100644 --- a/homeassistant/components/hassio/translations/lb.json +++ b/homeassistant/components/hassio/translations/lb.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "board": "Board", "disk_total": "Disk Toal", "disk_used": "Disk benotzt", "docker_version": "Docker Versioun", diff --git a/homeassistant/components/hyperion/translations/lb.json b/homeassistant/components/hyperion/translations/lb.json new file mode 100644 index 00000000000..6d86d67564c --- /dev/null +++ b/homeassistant/components/hyperion/translations/lb.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Service ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "auth_new_token_not_granted_error": "Nei erstallte Jeton ass net an der Hyperion UI accord\u00e9iert", + "auth_new_token_not_work_error": "Feeler bei der Authentifikatioun mam nei erstallte Jeton", + "auth_required_error": "Feeler beim best\u00ebmmen ob Autorisatioun erfuerderlech ass", + "cannot_connect": "Feeler beim verbannen", + "no_id": "D\u00ebs Hyperion Ambilight Instanz huet seng ID net gemellt." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_access_token": "Ong\u00ebltegen Acc\u00e8s Jeton" + }, + "step": { + "auth": { + "data": { + "create_token": "Neie Jeton automatesch erstellen", + "token": "oder scho bestehenden Jeton uginn" + }, + "description": "Autorisatioun mat dengem Hyperion Ambilight Server konfigur\u00e9ieren" + }, + "confirm": { + "description": "Soll d\u00ebsen Hyperion Ambilight am Home Assistant dob\u00e4i gesaat ginn?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Dob\u00e4isetzen vum Hyperion Ambilight Service best\u00e4tegen" + }, + "create_token": { + "description": "Klick **Ofsch\u00e9cken** fir een neien Authentifikatioun's Jeton unzefroen. Du g\u00ebss dann an Hyperion UI weidergeleet fir d'Ufro z'accord\u00e9ieren. Iwwerpr\u00e9if dass d\u00e9i ugewisen id \"{auth_id}\" ass.", + "title": "Neien Authentifikatioun's Jeton automatesch erstellen" + }, + "create_token_external": { + "title": "Neie Jeton an der Hyperion UI accord\u00e9ieren" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion Priorit\u00e9it fir Faarwen an Effekter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json new file mode 100644 index 00000000000..e705d115c8d --- /dev/null +++ b/homeassistant/components/hyperion/translations/pl.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "auth_new_token_not_granted_error": "Nowo utworzony token nie zosta\u0142 zatwierdzony w interfejsie u\u017cytkownika Hyperion", + "auth_new_token_not_work_error": "Nie uda\u0142o si\u0119 uwierzytelni\u0107 przy u\u017cyciu nowo utworzonego tokena", + "auth_required_error": "Nie uda\u0142o si\u0119 okre\u015bli\u0107, czy wymagana jest autoryzacja", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_id": "Instancja Hyperion Ambilight nie zg\u0142osi\u0142a swojego identyfikatora" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_access_token": "Niepoprawny token dost\u0119pu" + }, + "step": { + "auth": { + "data": { + "create_token": "Automatycznie utw\u00f3rz nowy token", + "token": "Lub podaj istniej\u0105cy token" + }, + "description": "Skonfiguruj autoryzacj\u0119 do serwera Hyperion Ambilight" + }, + "confirm": { + "description": "Czy chcesz doda\u0107 ten Hyperion Ambilight do Home Assistanta? \n\n **Host:** {host}\n **Port:** {port}\n **ID**: {id}", + "title": "Potwierdzanie dodania us\u0142ugi Hyperion Ambilight" + }, + "create_token": { + "description": "Naci\u015bnij **Prze\u015blij** poni\u017cej, aby za\u017c\u0105da\u0107 nowego tokena uwierzytelniania. Nast\u0105pi przekierowanie do interfejsu u\u017cytkownika Hyperion, aby zatwierdzi\u0107 \u017c\u0105danie. Sprawd\u017a, czy wy\u015bwietlany identyfikator to \u201e {auth_id} \u201d", + "title": "Automatyczne tworzenie nowego tokena uwierzytelniaj\u0105cego" + }, + "create_token_external": { + "title": "Akceptowanie nowego tokena w interfejsie u\u017cytkownika Hyperion" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion ma pierwsze\u0144stwo w u\u017cyciu dla kolor\u00f3w i efekt\u00f3w" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/pl.json b/homeassistant/components/ipma/translations/pl.json index 405b9fa81c6..9a10d541f1d 100644 --- a/homeassistant/components/ipma/translations/pl.json +++ b/homeassistant/components/ipma/translations/pl.json @@ -15,5 +15,10 @@ "title": "Lokalizacja" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Dost\u0119pno\u015b\u0107 punktu ko\u0144cowego API IPMA" + } } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/lb.json b/homeassistant/components/local_ip/translations/lb.json index 877f1781e04..c94685b1a2a 100644 --- a/homeassistant/components/local_ip/translations/lb.json +++ b/homeassistant/components/local_ip/translations/lb.json @@ -8,6 +8,7 @@ "data": { "name": "Numm vum Sensor" }, + "description": "Soll den Ariichtungs Prozess gestart ginn?", "title": "Lokal IP Adresse" } } diff --git a/homeassistant/components/logi_circle/translations/et.json b/homeassistant/components/logi_circle/translations/et.json index 3aff36b4061..c61fc76a5aa 100644 --- a/homeassistant/components/logi_circle/translations/et.json +++ b/homeassistant/components/logi_circle/translations/et.json @@ -8,7 +8,7 @@ }, "error": { "authorize_url_timeout": "Tuvastamise URL'i loomise ajal\u00f5pp.", - "follow_link": "Palun j\u00e4rgige linki ja tuvasta enne Submit vajutamist.", + "follow_link": "Palun j\u00e4rgi linki ja tuvasta enne Esita nupu vajutamist.", "invalid_auth": "Tuvastamine eba\u00f5nnestus" }, "step": { diff --git a/homeassistant/components/mobile_app/translations/cs.json b/homeassistant/components/mobile_app/translations/cs.json index 5b5a68db066..467536cc5ec 100644 --- a/homeassistant/components/mobile_app/translations/cs.json +++ b/homeassistant/components/mobile_app/translations/cs.json @@ -8,5 +8,10 @@ "description": "Chcete nastavit komponentu Mobiln\u00ed aplikace?" } } + }, + "device_automation": { + "action_type": { + "notify": "Odeslat ozn\u00e1men\u00ed" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json index c442cc88a26..43ba004ac72 100644 --- a/homeassistant/components/mobile_app/translations/es.json +++ b/homeassistant/components/mobile_app/translations/es.json @@ -8,5 +8,10 @@ "description": "\u00bfQuieres configurar el componente de la aplicaci\u00f3n para el m\u00f3vil?" } } + }, + "device_automation": { + "action_type": { + "notify": "Enviar una notificaci\u00f3n" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json index e5c01546976..41d2be9d455 100644 --- a/homeassistant/components/mobile_app/translations/et.json +++ b/homeassistant/components/mobile_app/translations/et.json @@ -8,5 +8,10 @@ "description": "Kas soovid seadistada mobiilirakenduse sidumist?" } } + }, + "device_automation": { + "action_type": { + "notify": "Saada teavitus" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/lb.json b/homeassistant/components/mobile_app/translations/lb.json index 6e15dd34634..e3674292888 100644 --- a/homeassistant/components/mobile_app/translations/lb.json +++ b/homeassistant/components/mobile_app/translations/lb.json @@ -8,5 +8,10 @@ "description": "Soll d'Mobil App konfigur\u00e9iert ginn?" } } + }, + "device_automation": { + "action_type": { + "notify": "Eng Notifikatioun sch\u00e9cken" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json index 346a5eb6b6d..65d465723b1 100644 --- a/homeassistant/components/mobile_app/translations/no.json +++ b/homeassistant/components/mobile_app/translations/no.json @@ -8,5 +8,10 @@ "description": "Vil du sette opp mobilapp-komponenten?" } } + }, + "device_automation": { + "action_type": { + "notify": "Send et varsel" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/pl.json b/homeassistant/components/mobile_app/translations/pl.json index c62d2c81076..cd083447634 100644 --- a/homeassistant/components/mobile_app/translations/pl.json +++ b/homeassistant/components/mobile_app/translations/pl.json @@ -8,5 +8,10 @@ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" } } + }, + "device_automation": { + "action_type": { + "notify": "wy\u015blij powiadomienie" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/zh-Hant.json b/homeassistant/components/mobile_app/translations/zh-Hant.json index e09710e4235..d54afd94f54 100644 --- a/homeassistant/components/mobile_app/translations/zh-Hant.json +++ b/homeassistant/components/mobile_app/translations/zh-Hant.json @@ -8,5 +8,10 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u624b\u6a5f App \u5143\u4ef6\uff1f" } } + }, + "device_automation": { + "action_type": { + "notify": "\u50b3\u9001\u901a\u77e5" + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json index b5c9c77397f..7a3dcfdbf07 100644 --- a/homeassistant/components/motion_blinds/translations/lb.json +++ b/homeassistant/components/motion_blinds/translations/lb.json @@ -10,7 +10,8 @@ "data": { "api_key": "API Schl\u00ebssel", "host": "IP Adresse" - } + }, + "title": "Steierbar Jalousien" } } } diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index 9c3ae442631..9ab94c993eb 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -36,5 +36,10 @@ "title": "Vyberte metodu ov\u011b\u0159en\u00ed" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Detekov\u00e1n pohyb" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 99066e26fd8..63e45df12fa 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -36,5 +36,13 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "nast\u0105pi wykrycie ruchu", + "camera_person": "nast\u0105pi wykrycie osoby", + "camera_sound": "nast\u0105pi wykrycie d\u017awi\u0119ku", + "doorbell_chime": "dzwonek zostanie wci\u015bni\u0119ty" + } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json index a298aa33671..6ddd2e7ab96 100644 --- a/homeassistant/components/ozw/translations/et.json +++ b/homeassistant/components/ozw/translations/et.json @@ -4,6 +4,8 @@ "addon_info_failed": "OpenZWave'i lisandmooduli teabe hankimine nurjus.", "addon_install_failed": "OpenZWave'i lisandmooduli paigaldamine nurjus.", "addon_set_config_failed": "OpenZWave'i konfiguratsiooni seadistamine eba\u00f5nnestus.", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", "mqtt_required": "MQTT sidumine pole seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, @@ -14,6 +16,9 @@ "install_addon": "Palun oota kuni OpenZWave lisandmooduli paigaldus l\u00f5peb. See v\u00f5ib v\u00f5tta mitu minutit." }, "step": { + "hassio_confirm": { + "title": "Seadista OpenZWave'i sidumine OpenZWave lisandmooduli abil" + }, "install_addon": { "title": "OpenZWave lisandmooduli paigaldamine on alanud" }, diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json index f63c4dd10b6..a143163ca1b 100644 --- a/homeassistant/components/ozw/translations/pl.json +++ b/homeassistant/components/ozw/translations/pl.json @@ -10,7 +10,13 @@ "error": { "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku OpenZWave. Sprawd\u017a konfiguracj\u0119." }, + "progress": { + "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku OpenZWave. Mo\u017ce to potrwa\u0107 kilka minut." + }, "step": { + "install_addon": { + "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "U\u017cyj dodatku OpenZWave Supervisor" diff --git a/homeassistant/components/person/translations/zh-Hans.json b/homeassistant/components/person/translations/zh-Hans.json index 16213ba3079..fd3c48e0b4e 100644 --- a/homeassistant/components/person/translations/zh-Hans.json +++ b/homeassistant/components/person/translations/zh-Hans.json @@ -5,5 +5,5 @@ "not_home": "\u79bb\u5f00" } }, - "title": "\u4e2a\u4eba" + "title": "\u4eba\u5458" } \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/lb.json b/homeassistant/components/vizio/translations/lb.json index 4eeb9ecbef6..029d75ea2cb 100644 --- a/homeassistant/components/vizio/translations/lb.json +++ b/homeassistant/components/vizio/translations/lb.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Apparat ass scho konfigur\u00e9iert", + "cannot_connect": "Feeler beim verbannen", "updated_entry": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9ierten Numm an/oder Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert." }, "error": { From 5e3941badb8cbc79e66efa5e9b0aec2a00144c55 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Dec 2020 20:51:51 +0100 Subject: [PATCH 368/430] Updated frontend to 20201202.0 (#43862) --- 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 cd9d4b3b5ba..34cd7edbf26 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201126.0"], + "requirements": ["home-assistant-frontend==20201202.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e19817bb2c4..3f85297fc79 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201126.0 +home-assistant-frontend==20201202.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0ec2442425e..39398e7b2e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201126.0 +home-assistant-frontend==20201202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09a722d716d..02bd6099457 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201126.0 +home-assistant-frontend==20201202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d518db1c95f8b43b719e794cb4f0c0164916993b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Wed, 2 Dec 2020 21:16:30 +0100 Subject: [PATCH 369/430] Improve custom datatype parsing in Modbus sensor and climate (#42354) --- .coveragerc | 1 + homeassistant/components/modbus/climate.py | 10 +++++- homeassistant/components/modbus/sensor.py | 36 ++++++++++++++++------ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index e5ee7b3dc92..76d48720110 100644 --- a/.coveragerc +++ b/.coveragerc @@ -547,6 +547,7 @@ omit = homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py homeassistant/components/modbus/switch.py + homeassistant/components/modbus/sensor.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index d57d0f761c6..b09a38f082e 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -255,7 +255,15 @@ class ModbusThermostat(ClimateEntity): byte_string = b"".join( [x.to_bytes(2, byteorder="big") for x in result.registers] ) - val = struct.unpack(self._structure, byte_string)[0] + val = struct.unpack(self._structure, byte_string) + if len(val) != 1 or not isinstance(val[0], (float, int)): + _LOGGER.error( + "Unable to parse result as a single int or float value; adjust your configuration. Result: %s", + str(val), + ) + return -1 + + val = val[0] register_value = format( (self._scale * val) + self._offset, f".{self._precision}f" ) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index bd6b5e349c3..656e5e2986d 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -250,16 +250,32 @@ class ModbusRegisterSensor(RestoreEntity): registers.reverse() byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) - if self._data_type != DATA_TYPE_STRING: - val = struct.unpack(self._structure, byte_string)[0] - val = self._scale * val + self._offset - if isinstance(val, int): - self._value = str(val) - if self._precision > 0: - self._value += "." + "0" * self._precision - else: - self._value = f"{val:.{self._precision}f}" - else: + if self._data_type == DATA_TYPE_STRING: self._value = byte_string.decode() + else: + val = struct.unpack(self._structure, byte_string) + + # Issue: https://github.com/home-assistant/core/issues/41944 + # If unpack() returns a tuple greater than 1, don't try to process the value. + # Instead, return the values of unpack(...) separated by commas. + if len(val) > 1: + self._value = ",".join(map(str, val)) + else: + val = val[0] + + # Apply scale and precision to floats and ints + if isinstance(val, (float, int)): + val = self._scale * val + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: + self._value = str(val) + else: + self._value = f"{float(val):.{self._precision}f}" + else: + # Don't process remaining datatypes (bytes and booleans) + self._value = str(val) self._available = True From 30baf333c358d1311dfd4af5ed90fe942f30f434 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Dec 2020 21:20:14 +0100 Subject: [PATCH 370/430] Improve handling of disabled devices (#43864) --- .../components/config/entity_registry.py | 13 ++++ homeassistant/helpers/entity_registry.py | 13 +++- .../components/config/test_entity_registry.py | 63 ++++++++++++++++++- tests/helpers/test_entity_registry.py | 34 ++++++++-- 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 73327ecf23c..8d1c488bfa0 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -107,6 +107,19 @@ async def websocket_update_entity(hass, connection, msg): ) return + if "disabled_by" in msg and msg["disabled_by"] is None: + entity = registry.entities[msg["entity_id"]] + if entity.device_id: + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entity.device_id) + if device.disabled: + connection.send_message( + websocket_api.error_message( + msg["id"], "invalid_info", "Device is disabled" + ) + ) + return + try: if changes: entry = registry.async_update_entity(msg["entity_id"], **changes) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7e8700e8236..4582fc5f3b6 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -311,11 +311,18 @@ class EntityRegistry: device_registry = await self.hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(event.data["device_id"]) if not device.disabled: + entities = async_entries_for_device( + self, event.data["device_id"], include_disabled_entities=True + ) + for entity in entities: + if entity.disabled_by != DISABLED_DEVICE: + continue + self.async_update_entity( # type: ignore + entity.entity_id, disabled_by=None + ) return - entities = async_entries_for_device( - self, event.data["device_id"], include_disabled_entities=True - ) + entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: self.async_update_entity( # type: ignore entity.entity_id, disabled_by=DISABLED_DEVICE diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index a506135c16d..93d33bc9562 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -7,7 +7,13 @@ from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.entity_registry import RegistryEntry -from tests.common import MockConfigEntry, MockEntity, MockEntityPlatform, mock_registry +from tests.common import ( + MockConfigEntry, + MockEntity, + MockEntityPlatform, + mock_device_registry, + mock_registry, +) @pytest.fixture @@ -17,6 +23,12 @@ def client(hass, hass_ws_client): yield hass.loop.run_until_complete(hass_ws_client(hass)) +@pytest.fixture +def device_registry(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + async def test_list_entities(hass, client): """Test list entries.""" entities = OrderedDict() @@ -282,6 +294,55 @@ async def test_update_entity_require_restart(hass, client): } +async def test_enable_entity_disabled_device(hass, client, device_registry): + """Test enabling entity of disabled device.""" + config_entry = MockConfigEntry(domain="test_platform") + config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id="1234", + connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + disabled_by="user", + ) + + mock_registry( + hass, + { + "test_domain.world": RegistryEntry( + config_entry_id=config_entry.entry_id, + entity_id="test_domain.world", + unique_id="1234", + # Using component.async_add_entities is equal to platform "domain" + platform="test_platform", + device_id=device.id, + ) + }, + ) + platform = MockEntityPlatform(hass) + entity = MockEntity(unique_id="1234") + await platform.async_add_entities([entity]) + + state = hass.states.get("test_domain.world") + assert state is not None + + # UPDATE DISABLED_BY TO NONE + await client.send_json( + { + "id": 8, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "disabled_by": None, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + + async def test_update_entity_no_changes(hass, client): """Test update entity with no changes.""" mock_registry( diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 960537e784c..19af3715160 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -711,31 +711,53 @@ async def test_remove_device_removes_entities(hass, registry): async def test_disable_device_disables_entities(hass, registry): - """Test that we remove entities tied to a device.""" + """Test that we disable entities tied to a device.""" device_registry = mock_device_registry(hass) config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={("mac", "12:34:56:AB:CD:EF")}, ) - entry = registry.async_get_or_create( + entry1 = registry.async_get_or_create( "light", "hue", "5678", config_entry=config_entry, device_id=device_entry.id, ) + entry2 = registry.async_get_or_create( + "light", + "hue", + "ABCD", + config_entry=config_entry, + device_id=device_entry.id, + disabled_by="user", + ) - assert not entry.disabled + assert not entry1.disabled + assert entry2.disabled device_registry.async_update_device(device_entry.id, disabled_by="user") await hass.async_block_till_done() - entry = registry.async_get(entry.entity_id) - assert entry.disabled - assert entry.disabled_by == "device" + entry1 = registry.async_get(entry1.entity_id) + assert entry1.disabled + assert entry1.disabled_by == "device" + entry2 = registry.async_get(entry2.entity_id) + assert entry2.disabled + assert entry2.disabled_by == "user" + + device_registry.async_update_device(device_entry.id, disabled_by=None) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.entity_id) + assert not entry1.disabled + entry2 = registry.async_get(entry2.entity_id) + assert entry2.disabled + assert entry2.disabled_by == "user" async def test_disabled_entities_excluded_from_entity_list(hass, registry): From ce056656f85e46d342223f0415c5e5ae94d3466e Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 2 Dec 2020 22:03:31 +0100 Subject: [PATCH 371/430] Implement new Google TTS API via dedicated library (#43863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/google_translate/manifest.json | 2 +- .../components/google_translate/tts.py | 176 ++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google_translate/test_tts.py | 301 +++++++----------- 5 files changed, 175 insertions(+), 308 deletions(-) diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 6d40b2f7a09..c5b3edc8798 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -2,6 +2,6 @@ "domain": "google_translate", "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": ["gTTS-token==1.1.4"], + "requirements": ["gTTS==2.2.1"], "codeowners": [] } diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 66c00008046..c9a5eef2c83 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -1,78 +1,95 @@ """Support for the Google speech service.""" -import asyncio +from io import BytesIO import logging -import re -import aiohttp -from aiohttp.hdrs import REFERER, USER_AGENT -import async_timeout -from gtts_token import gtts_token +from gtts import gTTS, gTTSError import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import HTTP_OK -from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -GOOGLE_SPEECH_URL = "https://translate.google.com/translate_tts" -MESSAGE_SIZE = 148 - SUPPORT_LANGUAGES = [ "af", - "sq", "ar", - "hy", "bn", + "bs", "ca", - "zh", - "zh-cn", - "zh-tw", - "zh-yue", - "hr", "cs", + "cy", "da", - "nl", - "en", - "en-au", - "en-uk", - "en-us", - "eo", - "fi", - "fr", "de", "el", + "en", + "eo", + "es", + "et", + "fi", + "fr", + "gu", "hi", + "hr", "hu", - "is", + "hy", "id", + "is", "it", "ja", + "jw", + "km", + "kn", "ko", "la", "lv", "mk", + "ml", + "mr", + "my", + "ne", + "nl", "no", "pl", "pt", - "pt-br", "ro", "ru", - "sr", + "si", "sk", - "es", - "es-es", - "es-mx", - "es-us", - "sw", + "sq", + "sr", + "su", "sv", + "sw", "ta", + "te", "th", + "tl", "tr", - "vi", - "cy", "uk", - "bg-BG", + "ur", + "vi", + # dialects + "zh-CN", + "zh-cn", + "zh-tw", + "en-us", + "en-ca", + "en-uk", + "en-gb", + "en-au", + "en-gh", + "en-in", + "en-ie", + "en-nz", + "en-ng", + "en-ph", + "en-za", + "en-tz", + "fr-ca", + "fr-fr", + "pt-br", + "pt-pt", + "es-es", + "es-us", ] DEFAULT_LANG = "en" @@ -94,14 +111,6 @@ class GoogleProvider(Provider): """Init Google TTS service.""" self.hass = hass self._lang = lang - self.headers = { - REFERER: "http://translate.google.com/", - 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.name = "Google" @property @@ -114,74 +123,15 @@ class GoogleProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - async def async_get_tts_audio(self, message, language, options=None): + def get_tts_audio(self, message, language, options=None): """Load TTS from google.""" + tts = gTTS(text=message, lang=language) + mp3_data = BytesIO() - token = gtts_token.Token() - websession = async_get_clientsession(self.hass) - message_parts = self._split_message_to_parts(message) + try: + tts.write_to_fp(mp3_data) + except gTTSError as exc: + _LOGGER.exception("Error during processing of TTS request %s", exc) + return None, None - data = b"" - for idx, part in enumerate(message_parts): - try: - part_token = await self.hass.async_add_executor_job( - token.calculate_token, part - ) - except ValueError as err: - # If token seed fetching fails. - _LOGGER.warning(err) - return None, None - - url_param = { - "ie": "UTF-8", - "tl": language, - "q": part, - "tk": part_token, - "total": len(message_parts), - "idx": idx, - "client": "tw-ob", - "textlen": len(part), - } - - try: - with async_timeout.timeout(10): - request = await websession.get( - GOOGLE_SPEECH_URL, params=url_param, headers=self.headers - ) - - if request.status != HTTP_OK: - _LOGGER.error( - "Error %d on load URL %s", request.status, request.url - ) - return None, None - data += await request.read() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout for google speech") - return None, None - - return "mp3", data - - @staticmethod - def _split_message_to_parts(message): - """Split message into single parts.""" - if len(message) <= MESSAGE_SIZE: - return [message] - - punc = "!()[]?.,;:" - punc_list = [re.escape(c) for c in punc] - pattern = "|".join(punc_list) - parts = re.split(pattern, message) - - def split_by_space(fullstring): - """Split a string by space.""" - if len(fullstring) > MESSAGE_SIZE: - idx = fullstring.rfind(" ", 0, MESSAGE_SIZE) - return [fullstring[:idx]] + split_by_space(fullstring[idx:]) - return [fullstring] - - msg_parts = [] - for part in parts: - msg_parts += split_by_space(part) - - return [msg for msg in msg_parts if len(msg) > 0] + return "mp3", mp3_data.getvalue() diff --git a/requirements_all.txt b/requirements_all.txt index 39398e7b2e9..cb93fc3e7c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ freesms==0.1.2 fritzconnection==1.3.4 # homeassistant.components.google_translate -gTTS-token==1.1.4 +gTTS==2.2.1 # homeassistant.components.garmin_connect garminconnect==0.1.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02bd6099457..df7cbdafc63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -308,7 +308,7 @@ fnvhash==0.1.0 foobot_async==1.0.0 # homeassistant.components.google_translate -gTTS-token==1.1.4 +gTTS==2.2.1 # homeassistant.components.garmin_connect garminconnect==0.1.16 diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 8e9ec9b7e1c..79c303fd2ff 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -1,8 +1,10 @@ """The tests for the Google speech platform.""" -import asyncio import os import shutil +from gtts import gTTSError +import pytest + from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, @@ -10,226 +12,141 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.async_mock import patch -from tests.common import assert_setup_component, get_test_home_assistant, mock_service +from tests.common import async_mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 -class TestTTSGooglePlatform: - """Test the Google speech component.""" +@pytest.fixture(autouse=True) +def cleanup_cache(hass): + """Clean up TTS cache.""" + yield + default_tts = hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - asyncio.run_coroutine_threadsafe( - async_process_ha_core_config( - self.hass, {"internal_url": "http://example.local:8123"} - ), - self.hass.loop, - ) +@pytest.fixture +async def calls(hass): + """Mock media player calls.""" + return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - self.url = "https://translate.google.com/translate_tts" - self.url_param = { - "tl": "en", - "q": "90%25%20of%20I%20person%20is%20on%20front%20of%20your%20door.", - "tk": 5, - "client": "tw-ob", - "textlen": 41, - "total": 1, - "idx": 0, - "ie": "UTF-8", - } - def teardown_method(self): - """Stop everything that was started.""" - default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) - if os.path.isdir(default_tts): - shutil.rmtree(default_tts) +@pytest.fixture(autouse=True) +async def setup_internal_url(hass): + """Set up internal url.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"} + ) - self.hass.stop() - def test_setup_component(self): - """Test setup component.""" - config = {tts.DOMAIN: {"platform": "google_translate"}} +@pytest.fixture +def mock_gtts(): + """Mock gtts.""" + with patch("homeassistant.components.google_translate.tts.gTTS") as mock_gtts: + yield mock_gtts - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say(self, mock_calculate, aioclient_mock): - """Test service call say.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_service_say(hass, mock_gtts, calls): + """Test service call say.""" - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + await async_setup_component( + hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}} + ) - config = {tts.DOMAIN: {"platform": "google_translate"}} + await hass.services.async_call( + tts.DOMAIN, + "google_translate_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert len(calls) == 1 + assert len(mock_gtts.mock_calls) == 2 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() + assert mock_gtts.mock_calls[0][2] == { + "text": "There is a person at the front door.", + "lang": "en", + } - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_german_config(self, mock_calculate, aioclient_mock): - """Test service call say with german code in the config.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_service_say_german_config(hass, mock_gtts, calls): + """Test service call say with german code in the config.""" - self.url_param["tl"] = "de" - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + await async_setup_component( + hass, + tts.DOMAIN, + {tts.DOMAIN: {"platform": "google_translate", "language": "de"}}, + ) - config = {tts.DOMAIN: {"platform": "google_translate", "language": "de"}} + await hass.services.async_call( + tts.DOMAIN, + "google_translate_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert len(calls) == 1 + assert len(mock_gtts.mock_calls) == 2 + assert mock_gtts.mock_calls[0][2] == { + "text": "There is a person at the front door.", + "lang": "de", + } - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 +async def test_service_say_german_service(hass, mock_gtts, calls): + """Test service call say with german code in the service.""" - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_german_service(self, mock_calculate, aioclient_mock): - """Test service call say with german code in the service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = { + tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"} + } - self.url_param["tl"] = "de" - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + await async_setup_component(hass, tts.DOMAIN, config) - config = { - tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"} - } + await hass.services.async_call( + tts.DOMAIN, + "google_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_LANGUAGE: "de", + }, + blocking=True, + ) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert len(calls) == 1 + assert len(mock_gtts.mock_calls) == 2 + assert mock_gtts.mock_calls[0][2] == { + "text": "There is a person at the front door.", + "lang": "de", + } - self.hass.services.call( - tts.DOMAIN, - "google_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - tts.ATTR_LANGUAGE: "de", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 +async def test_service_say_error(hass, mock_gtts, calls): + """Test service call say with http response 400.""" + mock_gtts.return_value.write_to_fp.side_effect = gTTSError + await async_setup_component( + hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}} + ) - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_error(self, mock_calculate, aioclient_mock): - """Test service call say with http response 400.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + await hass.services.async_call( + tts.DOMAIN, + "google_translate_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) - aioclient_mock.get(self.url, params=self.url_param, status=400, content=b"test") - - config = {tts.DOMAIN: {"platform": "google_translate"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() - - assert len(calls) == 0 - assert len(aioclient_mock.mock_calls) == 1 - - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_timeout(self, mock_calculate, aioclient_mock): - """Test service call say with http timeout.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - aioclient_mock.get(self.url, params=self.url_param, exc=asyncio.TimeoutError()) - - config = {tts.DOMAIN: {"platform": "google_translate"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() - - assert len(calls) == 0 - assert len(aioclient_mock.mock_calls) == 1 - - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_long_size(self, mock_calculate, aioclient_mock): - """Test service call say with a lot of text.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - self.url_param["total"] = 9 - self.url_param["q"] = "I%20person%20is%20on%20front%20of%20your%20door" - self.url_param["textlen"] = 33 - for idx in range(9): - self.url_param["idx"] = idx - aioclient_mock.get( - self.url, params=self.url_param, status=200, content=b"test" - ) - - config = { - tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"} - } - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "google_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: ( - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - ), - }, - ) - self.hass.block_till_done() - - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 9 - assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 + assert len(calls) == 0 + assert len(mock_gtts.mock_calls) == 2 From adcb0260e0c3cdbcf2ba6662e61a0af0290a6eca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Dec 2020 22:07:58 +0100 Subject: [PATCH 372/430] Bumped version to 1.0.0b0 --- homeassistant/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 469c0fa7fbb..0e779d06c0d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 0 -MINOR_VERSION = 119 -PATCH_VERSION = "0.dev0" +MAJOR_VERSION = 1 +MINOR_VERSION = 0 +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From ec90ebe136dc93b98dad7e245af0297b8229266b Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Wed, 2 Dec 2020 15:28:17 -0600 Subject: [PATCH 373/430] Add Kuler Sky Bluetooth floor lamp integration (#42372) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/kulersky/__init__.py | 44 +++ .../components/kulersky/config_flow.py | 29 ++ homeassistant/components/kulersky/const.py | 2 + homeassistant/components/kulersky/light.py | 210 ++++++++++++ .../components/kulersky/manifest.json | 12 + .../components/kulersky/strings.json | 13 + .../components/kulersky/translations/en.json | 13 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/kulersky/__init__.py | 1 + tests/components/kulersky/test_config_flow.py | 104 ++++++ tests/components/kulersky/test_light.py | 315 ++++++++++++++++++ 14 files changed, 751 insertions(+) create mode 100644 homeassistant/components/kulersky/__init__.py create mode 100644 homeassistant/components/kulersky/config_flow.py create mode 100644 homeassistant/components/kulersky/const.py create mode 100644 homeassistant/components/kulersky/light.py create mode 100644 homeassistant/components/kulersky/manifest.json create mode 100644 homeassistant/components/kulersky/strings.json create mode 100644 homeassistant/components/kulersky/translations/en.json create mode 100644 tests/components/kulersky/__init__.py create mode 100644 tests/components/kulersky/test_config_flow.py create mode 100644 tests/components/kulersky/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index fe3af4c1ee6..c6deb8e9f8f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -242,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py new file mode 100644 index 00000000000..ff984e2c0d3 --- /dev/null +++ b/homeassistant/components/kulersky/__init__.py @@ -0,0 +1,44 @@ +"""Kuler Sky lights integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Kuler Sky component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Kuler Sky from a config entry.""" + 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/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py new file mode 100644 index 00000000000..2b22fcdbd31 --- /dev/null +++ b/homeassistant/components/kulersky/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Kuler Sky.""" +import logging + +import pykulersky + +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) -> bool: + """Return if there are devices that can be discovered.""" + # Check if there are any devices that can be discovered in the network. + try: + devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + except pykulersky.PykulerskyException as exc: + _LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc) + return False + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py new file mode 100644 index 00000000000..ae1e7a435dc --- /dev/null +++ b/homeassistant/components/kulersky/const.py @@ -0,0 +1,2 @@ +"""Constants for the Kuler Sky integration.""" +DOMAIN = "kulersky" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py new file mode 100644 index 00000000000..4c17d1bcba3 --- /dev/null +++ b/homeassistant/components/kulersky/light.py @@ -0,0 +1,210 @@ +"""Kuler Sky light platform.""" +import asyncio +from datetime import timedelta +import logging +from typing import Callable, List + +import pykulersky + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE + +DISCOVERY_INTERVAL = timedelta(seconds=60) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Kuler sky light devices.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if "devices" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["devices"] = set() + if "discovery" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["discovery"] = asyncio.Lock() + + async def discover(*args): + """Attempt to discover new lights.""" + # Since discovery needs to connect to all discovered bluetooth devices, and + # only rules out devices after a timeout, it can potentially take a long + # time. If there's already a discovery running, just skip this poll. + if hass.data[DOMAIN]["discovery"].locked(): + return + + async with hass.data[DOMAIN]["discovery"]: + bluetooth_devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + + # Filter out already connected lights + new_devices = [ + device + for device in bluetooth_devices + if device["address"] not in hass.data[DOMAIN]["devices"] + ] + + for device in new_devices: + light = pykulersky.Light(device["address"], device["name"]) + try: + # Attempt to connect to this light and read the color. If the + # connection fails, either this is not a Kuler Sky light, or + # it's bluetooth connection is currently locked by another + # device. If the vendor's app is connected to the light when + # home assistant tries to connect, this connection will fail. + await hass.async_add_executor_job(light.connect) + await hass.async_add_executor_job(light.get_color) + except pykulersky.PykulerskyException: + continue + # The light has successfully connected + hass.data[DOMAIN]["devices"].add(device["address"]) + async_add_entities([KulerskyLight(light)], update_before_add=True) + + # Start initial discovery + hass.async_add_job(discover) + + # Perform recurring discovery of new devices + async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + + +class KulerskyLight(LightEntity): + """Representation of an Kuler Sky Light.""" + + def __init__(self, light: pykulersky.Light): + """Initialize a Kuler Sky light.""" + self._light = light + self._hs_color = None + self._brightness = None + self._white_value = None + self._available = True + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove( + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + await self.hass.async_add_executor_job(self.disconnect) + + def disconnect(self, *args) -> None: + """Disconnect the underlying device.""" + self._light.disconnect() + + @property + def name(self): + """Return the display name of this light.""" + return self._light.name + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._light.address + + @property + def device_info(self): + """Device info for this light.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Brightech", + } + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_KULERSKY + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def is_on(self): + """Return true if light is on.""" + return self._brightness > 0 or self._white_value > 0 + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + default_hs = (0, 0) if self._hs_color is None else self._hs_color + hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + + default_brightness = 0 if self._brightness is None else self._brightness + brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) + + default_white_value = 255 if self._white_value is None else self._white_value + white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value) + + if brightness == 0 and white_value == 0 and not kwargs: + # If the light would be off, and no additional parameters were + # passed, just turn the light on full brightness. + brightness = 255 + white_value = 255 + + rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + + self._light.set_color(*rgb, white_value) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.set_color(0, 0, 0, 0) + + def update(self): + """Fetch new state data for this light.""" + try: + if not self._light.connected: + self._light.connect() + # pylint: disable=invalid-name + r, g, b, w = self._light.get_color() + except pykulersky.PykulerskyException as exc: + if self._available: + _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) + self._available = False + return + if not self._available: + _LOGGER.info("Reconnected to %s", self.entity_id) + self._available = True + + hsv = color_util.color_RGB_to_hsv(r, g, b) + self._hs_color = hsv[:2] + self._brightness = int(round((hsv[2] / 100) * 255)) + self._white_value = w diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json new file mode 100644 index 00000000000..4f445e4fc18 --- /dev/null +++ b/homeassistant/components/kulersky/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "kulersky", + "name": "Kuler Sky", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kulersky", + "requirements": [ + "pykulersky==0.4.0" + ], + "codeowners": [ + "@emlove" + ] +} diff --git a/homeassistant/components/kulersky/strings.json b/homeassistant/components/kulersky/strings.json new file mode 100644 index 00000000000..ad8f0f41ae7 --- /dev/null +++ b/homeassistant/components/kulersky/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/kulersky/translations/en.json b/homeassistant/components/kulersky/translations/en.json new file mode 100644 index 00000000000..f05becffed3 --- /dev/null +++ b/homeassistant/components/kulersky/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a8e871aa02e..833f11190b6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -108,6 +108,7 @@ FLOWS = [ "juicenet", "kodi", "konnected", + "kulersky", "life360", "lifx", "local_ip", diff --git a/requirements_all.txt b/requirements_all.txt index cb93fc3e7c9..725bc5f0c09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1475,6 +1475,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df7cbdafc63..c3f8ed3c3b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,6 +745,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.lastfm pylast==4.0.0 diff --git a/tests/components/kulersky/__init__.py b/tests/components/kulersky/__init__.py new file mode 100644 index 00000000000..2b723b28fbd --- /dev/null +++ b/tests/components/kulersky/__init__.py @@ -0,0 +1 @@ +"""Tests for the Kuler Sky integration.""" diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py new file mode 100644 index 00000000000..59e3188fd7e --- /dev/null +++ b/tests/components/kulersky/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Kuler Sky config flow.""" +import pykulersky + +from homeassistant import config_entries, setup +from homeassistant.components.kulersky.config_flow import DOMAIN + +from tests.async_mock import patch + + +async def test_flow_success(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.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Kuler Sky" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_no_devices_found(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.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_flow_exceptions_caught(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.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + side_effect=pykulersky.PykulerskyException("TEST"), + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py new file mode 100644 index 00000000000..1b2472d7d7f --- /dev/null +++ b/tests/components/kulersky/test_light.py @@ -0,0 +1,315 @@ +"""Test the Kuler Sky lights.""" +import asyncio + +import pykulersky +import pytest + +from homeassistant import setup +from homeassistant.components.kulersky.light import DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + SCAN_INTERVAL, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +import homeassistant.util.dt as dt_util + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +async def mock_entry(hass): + """Create a mock light entity.""" + return MockConfigEntry(domain=DOMAIN) + + +@pytest.fixture +async def mock_light(hass, mock_entry): + """Create a mock light entity.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + light, "get_color", return_value=(0, 0, 0, 0) + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_connect.called + light.connected = True + + yield light + + +async def test_init(hass, mock_light): + """Test platform setup.""" + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object(hass.loop, "stop"), patch.object( + mock_light, "disconnect" + ) as mock_disconnect: + await hass.async_stop() + await hass.async_block_till_done() + + assert mock_disconnect.called + + +async def test_discovery_lock(hass, mock_entry): + """Test discovery lock.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + discovery_finished = None + first_discovery_started = asyncio.Event() + + async def mock_discovery(*args): + """Block to simulate multiple discovery calls while one still running.""" + nonlocal discovery_finished + if discovery_finished: + first_discovery_started.set() + await discovery_finished.wait() + return [] + + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.light.async_track_time_interval", + ) as mock_track_time_interval: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + with patch.object( + hass, "async_add_executor_job", side_effect=mock_discovery + ) as mock_run_discovery: + discovery_coroutine = mock_track_time_interval.call_args[0][1] + + discovery_finished = asyncio.Event() + + # Schedule multiple discoveries + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + + # Wait until the first discovery call is blocked + await first_discovery_started.wait() + + # Unblock the first discovery + discovery_finished.set() + + # Flush the remaining jobs + await hass.async_block_till_done() + + # The discovery method should only have been called once + mock_run_discovery.assert_called_once() + + +async def test_discovery_connection_error(hass, mock_entry): + """Test that invalid devices are skipped.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object( + light, "connect", side_effect=pykulersky.PykulerskyException + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert entity was not added + state = hass.states.get("light.bedroom") + assert state is None + + +async def test_remove_entry(hass, mock_light, mock_entry): + """Test platform setup.""" + with patch.object(mock_light, "disconnect") as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + +async def test_update_exception(hass, mock_light): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException + ): + await hass.helpers.entity_component.async_update_entity("light.bedroom") + state = hass.states.get("light.bedroom") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_light_turn_on(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(255, 255, 255, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(255, 255, 255, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 50, 50, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 50, 50, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 45, 25, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(50, 45, 25, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(220, 201, 110, 180) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 45, 25, 180) + + +async def test_light_turn_off(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(0, 0, 0, 0) + ): + await hass.services.async_call( + "light", + "turn_off", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(0, 0, 0, 0) + + +async def test_light_update(hass, mock_light): + """Test KulerSkyLight update.""" + utcnow = dt_util.utcnow() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + # Test an exception during discovery + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST") + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_UNAVAILABLE + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object( + mock_light, + "get_color", + return_value=(80, 160, 200, 240), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_ON + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + ATTR_BRIGHTNESS: 200, + ATTR_HS_COLOR: (200, 60), + ATTR_RGB_COLOR: (102, 203, 255), + ATTR_WHITE_VALUE: 240, + ATTR_XY_COLOR: (0.184, 0.261), + } From 15b775a4d374500f234ecbc9268dfd35825a834b Mon Sep 17 00:00:00 2001 From: tehbrd Date: Thu, 3 Dec 2020 10:58:10 +1000 Subject: [PATCH 374/430] Fix intesishome passing coroutine to HassJob (#43837) * Update climate.py Not allowed to pass coroutines to hassjob. * Update climate.py * Lint Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/intesishome/climate.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 57912d7d24d..a41161c7a6e 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -374,9 +374,11 @@ class IntesisAC(ClimateEntity): reconnect_minutes, ) # Schedule reconnection - async_call_later( - self.hass, reconnect_minutes * 60, self._controller.connect() - ) + + async def try_connect(_now): + await self._controller.connect() + + async_call_later(self.hass, reconnect_minutes * 60, try_connect) if self._controller.is_connected and not self._connected: # Connection has been restored From bf4e98d1ae9accb232b02ceb1a1a28bf376a3c66 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 2 Dec 2020 20:45:08 -0700 Subject: [PATCH 375/430] Fix Slack "invalid_blocks_format" bug (#43875) * Fix Slack "invalid_blocks_format" bug * Fix optional params * Fix one more optional param * Update manifest --- CODEOWNERS | 1 + homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/slack/notify.py | 27 ++++++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c6deb8e9f8f..27614c3d49d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -404,6 +404,7 @@ homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn +homeassistant/components/slack/* @bachya homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smappee/* @bsmappee diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index ad45abbe3c0..e183dd455f1 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -3,5 +3,5 @@ "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], - "codeowners": [] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 88317b31585..90caad62a58 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -198,17 +198,21 @@ class SlackNotificationService(BaseNotificationService): _LOGGER.error("Error while uploading file message: %s", err) async def _async_send_text_only_message( - self, targets, message, title, blocks, username, icon + self, + targets, + message, + title, + *, + username=None, + icon=None, + blocks=None, ): """Send a text-only message.""" - message_dict = { - "blocks": blocks, - "link_names": True, - "text": message, - "username": username, - } + message_dict = {"link_names": True, "text": message} + + if username: + message_dict["username"] = username - icon = icon or self._icon if icon: if icon.lower().startswith(("http://", "https://")): icon_type = "url" @@ -217,6 +221,9 @@ class SlackNotificationService(BaseNotificationService): message_dict[f"icon_{icon_type}"] = icon + if blocks: + message_dict["blocks"] = blocks + tasks = { target: self._client.chat_postMessage(**message_dict, channel=target) for target in targets @@ -256,15 +263,15 @@ class SlackNotificationService(BaseNotificationService): elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: - blocks = {} + blocks = None return await self._async_send_text_only_message( targets, message, title, - blocks, username=data.get(ATTR_USERNAME, self._username), icon=data.get(ATTR_ICON, self._icon), + blocks=blocks, ) # Message Type 2: A message that uploads a remote file From 3ee1aed06f71558422b74ac79c9ccfeded58679e Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Thu, 3 Dec 2020 21:57:35 -0500 Subject: [PATCH 376/430] Bump auroranoaa library to 0.0.2 (#43898) --- homeassistant/components/aurora/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 20f9e82dcb0..8d7d856e50c 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/aurora", "config_flow": true, "codeowners": ["@djtimca"], - "requirements": ["auroranoaa==0.0.1"] + "requirements": ["auroranoaa==0.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 725bc5f0c09..506aabc1a53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -291,7 +291,7 @@ asyncpysupla==0.0.5 atenpdu==0.3.0 # homeassistant.components.aurora -auroranoaa==0.0.1 +auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3f8ed3c3b7..1c7fbdae683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ arcam-fmj==0.5.3 async-upnp-client==0.14.13 # homeassistant.components.aurora -auroranoaa==0.0.1 +auroranoaa==0.0.2 # homeassistant.components.stream av==8.0.2 From 6c5911d37f33db863bd5e87af1888bc787299841 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Dec 2020 16:44:18 +0100 Subject: [PATCH 377/430] Blueprint: descriptions + descriptive errors (#43899) --- .../components/automation/blueprints/motion_light.yaml | 3 ++- .../automation/blueprints/notify_leaving_zone.yaml | 4 +++- homeassistant/components/blueprint/importer.py | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index c10d3691e6b..c11d22d974e 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -1,5 +1,6 @@ blueprint: name: Motion-activated Light + description: Turn on a light when motion is detected. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml input: @@ -17,7 +18,7 @@ blueprint: domain: light no_motion_wait: name: Wait time - description: Time to wait until the light should be turned off. + description: Time to leave the light on after last motion is detected. default: 120 selector: number: diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 9b79396f066..d3a70d773ee 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -1,5 +1,6 @@ blueprint: - name: Send notification when a person leaves a zone + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml input: @@ -26,6 +27,7 @@ trigger: variables: zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. zone_state: "{{ states[zone_entity].name }}" person_entity: !input person_entity person_name: "{{ states[person_entity].name }}" diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index bc40f76e7c2..524b04293ee 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -124,7 +124,9 @@ def _extract_blueprint_from_community_topic( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the topic") + raise HomeAssistantError( + "No valid blueprint found in the topic. Blueprint syntax blocks need to be marked as YAML or no syntax." + ) return ImportedBlueprint( f'{post["username"]}/{topic["slug"]}', block_content, blueprint @@ -204,7 +206,9 @@ async def fetch_blueprint_from_github_gist_url( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the gist") + raise HomeAssistantError( + "No valid blueprint found in the gist. The blueprint file needs to end with '.yaml'" + ) return ImportedBlueprint( f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint From b174c1d4eb49561c7eb357bf3ce33a64d29f24ed Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 3 Dec 2020 19:40:33 +0100 Subject: [PATCH 378/430] Unsubscribe ozw stop listener on entry unload (#43900) --- homeassistant/components/ozw/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index c0d50e18abc..1f46e7a17c6 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -279,7 +279,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except asyncio.CancelledError: pass - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client) + ozw_data[DATA_UNSUBSCRIBE].append( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client + ) + ) ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client else: From eb6128b53e0a9846dc18da17b9d2d95932501e14 Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Thu, 3 Dec 2020 11:08:16 -0600 Subject: [PATCH 379/430] Kulersky cleanups (#43901) --- .../components/kulersky/config_flow.py | 2 +- homeassistant/components/kulersky/light.py | 21 ++++++++++++------- tests/components/kulersky/test_light.py | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py index 2b22fcdbd31..04f7719b8e6 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -25,5 +25,5 @@ async def _async_has_devices(hass) -> bool: config_entry_flow.register_discovery_flow( - DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 4c17d1bcba3..71dd4a158ca 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -33,6 +33,12 @@ DISCOVERY_INTERVAL = timedelta(seconds=60) PARALLEL_UPDATES = 0 +def check_light(light: pykulersky.Light): + """Attempt to connect to this light and read the color.""" + light.connect() + light.get_color() + + async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, @@ -69,13 +75,12 @@ async def async_setup_entry( for device in new_devices: light = pykulersky.Light(device["address"], device["name"]) try: - # Attempt to connect to this light and read the color. If the - # connection fails, either this is not a Kuler Sky light, or - # it's bluetooth connection is currently locked by another - # device. If the vendor's app is connected to the light when - # home assistant tries to connect, this connection will fail. - await hass.async_add_executor_job(light.connect) - await hass.async_add_executor_job(light.get_color) + # If the connection fails, either this is not a Kuler Sky + # light, or it's bluetooth connection is currently locked + # by another device. If the vendor's app is connected to + # the light when home assistant tries to connect, this + # connection will fail. + await hass.async_add_executor_job(check_light, light) except pykulersky.PykulerskyException: continue # The light has successfully connected @@ -83,7 +88,7 @@ async def async_setup_entry( async_add_entities([KulerskyLight(light)], update_before_add=True) # Start initial discovery - hass.async_add_job(discover) + hass.async_create_task(discover()) # Perform recurring discovery of new devices async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 1b2472d7d7f..5403f7cedde 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -56,11 +56,11 @@ async def mock_light(hass, mock_entry): ], ): with patch( - "homeassistant.components.kulersky.light.pykulersky.Light" - ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + "homeassistant.components.kulersky.light.pykulersky.Light", + return_value=light, + ), patch.object(light, "connect") as mock_connect, patch.object( light, "get_color", return_value=(0, 0, 0, 0) ): - mockdevice.return_value = light mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From 6ae46268a421d1684387f411d5ac4b1868c1c62c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Dec 2020 22:41:02 +0100 Subject: [PATCH 380/430] Updated frontend to 20201203.0 (#43907) --- 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 34cd7edbf26..1bf6cdc580a 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201202.0"], + "requirements": ["home-assistant-frontend==20201203.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f85297fc79..65f228f5a0c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 506aabc1a53..4f2c23c8b45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c7fbdae683..101a47f4123 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From fb005ba3e289fdfd5e56502a612ca872f7f968df Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Dec 2020 03:16:37 +0100 Subject: [PATCH 381/430] Bump hatasmota to 0.1.4 (#43912) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 205 ++++++++++++++++++ 4 files changed, 208 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a4d6ec6036f..a4c6f77fc13 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.2"], + "requirements": ["hatasmota==0.1.4"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 4f2c23c8b45..26cc43bc504 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 101a47f4123..e88443140fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 627eb5198aa..f09c27da753 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -493,6 +493,191 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_OFF +async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["so"]["17"] = 0 # Hex color in state updates + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 255, 128) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + +async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["ty"] = 1 # Tuya device + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -745,6 +930,26 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->100: Speed should be capped at 40 + await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->0: Speed should be 1 + await common.async_turn_on(hass, "light.test", brightness=0, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 1;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->50: Speed should be 4*2*2=16 await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( From 698ee59f1011ae6293e7d4ed8d2bbbc56ce24c6e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Dec 2020 20:23:20 +0100 Subject: [PATCH 382/430] Always send ozw network key to add-on config (#43938) --- homeassistant/components/ozw/config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 4543bc27984..887560b154e 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -147,9 +147,10 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.network_key = user_input[CONF_NETWORK_KEY] self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = {CONF_ADDON_DEVICE: self.usb_path} - if self.network_key: - new_addon_config[CONF_ADDON_NETWORK_KEY] = self.network_key + new_addon_config = { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + } if new_addon_config != self.addon_config: await self._async_set_addon_config(new_addon_config) From 46b13e20ac6cd5fccde600db1e369817a81b8718 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Dec 2020 20:41:08 +0100 Subject: [PATCH 383/430] Handle stale ozw discovery flow (#43939) --- homeassistant/components/ozw/config_flow.py | 12 ++-- tests/components/ozw/test_config_flow.py | 66 +++++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 887560b154e..7c7c6e65dfe 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -58,17 +58,14 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - addon_config = await self._async_get_addon_config() - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return await self.async_step_hassio_confirm() async def async_step_hassio_confirm(self, user_input=None): """Confirm the add-on discovery.""" if user_input is not None: - self.use_addon = True - return self._async_create_entry_from_vars() + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) return self.async_show_form(step_id="hassio_confirm") @@ -107,6 +104,9 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True if await self._async_is_addon_running(): + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") return self._async_create_entry_from_vars() if await self._async_is_addon_installed(): diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 289b6c7f4cd..e86232adc65 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -159,9 +159,10 @@ async def test_not_addon(hass, supervisor): assert len(mock_setup_entry.mock_calls) == 1 -async def test_addon_running(hass, supervisor, addon_running): +async def test_addon_running(hass, supervisor, addon_running, addon_options): """Test add-on already running on Supervisor.""" - hass.config.components.add("mqtt") + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -182,8 +183,8 @@ async def test_addon_running(hass, supervisor, addon_running): assert result["type"] == "create_entry" assert result["title"] == TITLE assert result["data"] == { - "usb_path": None, - "network_key": None, + "usb_path": "/test", + "network_key": "abc123", "use_addon": True, "integration_created_addon": False, } @@ -193,7 +194,6 @@ async def test_addon_running(hass, supervisor, addon_running): async def test_addon_info_failure(hass, supervisor, addon_info): """Test add-on info failure.""" - hass.config.components.add("mqtt") addon_info.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -213,7 +213,6 @@ async def test_addon_installed( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on already installed but not running on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -250,7 +249,6 @@ async def test_set_addon_config_failure( hass, supervisor, addon_installed, addon_options, set_addon_options ): """Test add-on set config failure.""" - hass.config.components.add("mqtt") set_addon_options.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -273,7 +271,6 @@ async def test_start_addon_failure( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on start failure.""" - hass.config.components.add("mqtt") start_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -302,7 +299,6 @@ async def test_addon_not_installed( start_addon, ): """Test add-on not installed.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None await setup.async_setup_component(hass, "persistent_notification", {}) @@ -348,7 +344,6 @@ async def test_addon_not_installed( async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): """Test add-on install failure.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -488,3 +483,54 @@ async def test_abort_discovery_with_existing_entry( assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_discovery_addon_not_running( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test discovery with add-on already installed but not running.""" + addon_options["device"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "start_addon" + assert result["type"] == "form" + + +async def test_discovery_addon_not_installed( + hass, supervisor, addon_installed, install_addon, addon_options +): + """Test discovery with add-on not installed.""" + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "install_addon" + assert result["type"] == "progress" + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" From bf6bd969a2fbf0a2d8a619430bc10e730e359b56 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 5 Dec 2020 05:32:49 -0500 Subject: [PATCH 384/430] Return unique id of Blink binary sensor (#43942) --- homeassistant/components/blink/binary_sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 1841dbbc438..f69c94f0f5e 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -44,6 +44,11 @@ class BlinkBinarySensor(BinarySensorEntity): """Return the name of the blink sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + @property def device_class(self): """Return the class of this device.""" From 4c2dfa54da6f2863f06b193f7fe58abfbf32ffae Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Dec 2020 23:04:31 +0100 Subject: [PATCH 385/430] Updated frontend to 20201204.0 (#43945) --- 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 1bf6cdc580a..65fe745e51d 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201203.0"], + "requirements": ["home-assistant-frontend==20201204.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 65f228f5a0c..d820f5e715b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 26cc43bc504..c063877e817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e88443140fc..31578ce6b89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 625f219d6bfe25326be76356cbcfdff51d0a9640 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Dec 2020 11:53:43 +0100 Subject: [PATCH 386/430] Fix device refresh service can always add devices (#43950) --- homeassistant/components/deconz/gateway.py | 6 ++++-- homeassistant/components/deconz/services.py | 8 +++---- tests/components/deconz/test_binary_sensor.py | 21 ++++++++++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index dc41bb778ec..881ea883c4c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -121,9 +121,11 @@ class DeconzGateway: async_dispatcher_send(self.hass, self.signal_reachable, True) @callback - def async_add_device_callback(self, device_type, device=None) -> None: + def async_add_device_callback( + self, device_type, device=None, force: bool = False + ) -> None: """Handle event of new device creation in deCONZ.""" - if not self.option_allow_new_devices: + if not force and not self.option_allow_new_devices: return args = [] diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 2c286fac0a1..d524354ff0b 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -146,10 +146,10 @@ async def async_refresh_devices_service(hass, data): await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP) - gateway.async_add_device_callback(NEW_LIGHT) - gateway.async_add_device_callback(NEW_SCENE) - gateway.async_add_device_callback(NEW_SENSOR) + gateway.async_add_device_callback(NEW_GROUP, force=True) + gateway.async_add_device_callback(NEW_LIGHT, force=True) + gateway.async_add_device_callback(NEW_SCENE, force=True) + gateway.async_add_device_callback(NEW_SENSOR, force=True) async def async_remove_orphaned_entries_service(hass, data): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 5038c5bf3f2..78a4f1e937d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -10,15 +10,19 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, + CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SENSORS = { "1": { "id": "Presence sensor id", @@ -172,7 +176,7 @@ async def test_add_new_binary_sensor_ignored(hass): """Test that adding a new binary sensor is not allowed.""" config_entry = await setup_deconz_integration( hass, - options={CONF_ALLOW_NEW_DEVICES: False}, + options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 @@ -188,8 +192,23 @@ async def test_add_new_binary_sensor_ignored(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + assert not hass.states.get("binary_sensor.presence_sensor") entity_registry = await hass.helpers.entity_registry.async_get_registry() assert ( len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 ) + + with patch( + "pydeconz.DeconzSession.request", + return_value={ + "groups": {}, + "lights": {}, + "sensors": {"1": deepcopy(SENSORS["1"])}, + }, + ): + await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("binary_sensor.presence_sensor") From a390b0aca883d9dfbba137c0c45f63a9151480a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Dec 2020 13:15:45 +0100 Subject: [PATCH 387/430] Bumped version to 1.0.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0e779d06c0d..a34d9d1bff0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 6cb1ce056aeb99d5e182364d835e0daa975a5304 Mon Sep 17 00:00:00 2001 From: treylok Date: Sat, 5 Dec 2020 07:13:46 -0600 Subject: [PATCH 388/430] Fix Ecobee set humidity (#43954) --- homeassistant/components/ecobee/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index ccfddca4b03..94396bbf883 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate.const import ( SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) @@ -161,6 +162,7 @@ SUPPORT_FLAGS = ( | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE + | SUPPORT_TARGET_HUMIDITY ) @@ -651,7 +653,7 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity): """Set the humidity level.""" - self.data.ecobee.set_humidity(self.thermostat_index, humidity) + self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" From 4780bdd48f226b8ee3af4b3e319a6e2f3c32f7ab Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 7 Dec 2020 10:27:33 +0200 Subject: [PATCH 389/430] Prevent firing Shelly input events at startup (#43986) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 3c34da574a6..298c7e111b2 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -158,7 +158,11 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): last_event_count = self._last_input_events_count.get(channel) self._last_input_events_count[channel] = block.inputEventCnt - if last_event_count == block.inputEventCnt or event_type == "": + if ( + last_event_count is None + or last_event_count == block.inputEventCnt + or event_type == "" + ): continue if event_type in INPUTS_EVENTS_DICT: From 591777455a785310f5a81c704fae60fd1c0b268d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Dec 2020 17:24:32 +0100 Subject: [PATCH 390/430] Update ring to 0.6.2 (#43995) --- 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 d46f12af511..550da4d38ec 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.6.0"], + "requirements": ["ring_doorbell==0.6.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index c063877e817..839a5bd0e39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ rfk101py==0.0.1 rflink==0.0.55 # homeassistant.components.ring -ring_doorbell==0.6.0 +ring_doorbell==0.6.2 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31578ce6b89..1691ccdb263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ restrictedpython==5.0 rflink==0.0.55 # homeassistant.components.ring -ring_doorbell==0.6.0 +ring_doorbell==0.6.2 # homeassistant.components.roku rokuecp==0.6.0 From c7ec7e7c9845120e5d653ad4320ce5ecb8b28cb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Dec 2020 09:11:57 +0000 Subject: [PATCH 391/430] Bumped version to 1.0.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a34d9d1bff0..8aa48b5178c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 154fd4aa8bdac103dcef18263a4ff54b4495747b Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 7 Dec 2020 11:20:22 +0100 Subject: [PATCH 392/430] Fix Solaredge integration in case the data is not complete (#43557) Co-authored-by: Martin Hjelmare --- homeassistant/components/solaredge/sensor.py | 29 ++++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 0cd498b4e3b..e3e59676bf5 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -425,22 +425,21 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): self.data = {} self.attributes = {} self.unit = energy_details["unit"] - meters = energy_details["meters"] - for entity in meters: - for key, data in entity.items(): - if key == "type" and data in [ - "Production", - "SelfConsumption", - "FeedIn", - "Purchased", - "Consumption", - ]: - energy_type = data - if key == "values": - for row in data: - self.data[energy_type] = row["value"] - self.attributes[energy_type] = {"date": row["date"]} + for meter in energy_details["meters"]: + if "type" not in meter or "values" not in meter: + continue + if meter["type"] not in [ + "Production", + "SelfConsumption", + "FeedIn", + "Purchased", + "Consumption", + ]: + continue + if len(meter["values"][0]) == 2: + self.data[meter["type"]] = meter["values"][0]["value"] + self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} _LOGGER.debug( "Updated SolarEdge energy details: %s, %s", self.data, self.attributes From a1c55374605b7ab4218c5955efa3e68b02f87a63 Mon Sep 17 00:00:00 2001 From: Nigel Rook Date: Mon, 7 Dec 2020 12:14:54 +0000 Subject: [PATCH 393/430] Update generic_thermostat current_temperature on startup (#43951) Co-authored-by: Martin Hjelmare --- .../components/generic_thermostat/climate.py | 10 +++++--- .../generic_thermostat/test_climate.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 4072c43bc27..175ee8f1d5b 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, callback +from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -207,7 +207,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ) @callback - def _async_startup(event): + def _async_startup(*_): """Init on startup.""" sensor_state = self.hass.states.get(self.sensor_entity_id) if sensor_state and sensor_state.state not in ( @@ -215,8 +215,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity): STATE_UNKNOWN, ): self._async_update_temp(sensor_state) + self.async_write_ha_state() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + if self.hass.state == CoreState.running: + _async_startup() + else: + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index eaf7c8e5651..71c6f41282b 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -209,6 +209,30 @@ async def test_setup_defaults_to_unknown(hass): assert HVAC_MODE_OFF == hass.states.get(ENTITY).state +async def test_setup_gets_current_temp_from_sensor(hass): + """Test that current temperature is updated on entity addition.""" + hass.config.units = METRIC_SYSTEM + _setup_sensor(hass, 18) + await hass.async_block_till_done() + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_temp": 16, + } + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY).attributes["current_temperature"] == 18 + + async def test_default_setup_params(hass, setup_comp_2): """Test the setup with default parameters.""" state = hass.states.get(ENTITY) From 4b8e3171a3576e09c448e6df5bf7db841e4e20d5 Mon Sep 17 00:00:00 2001 From: Alex Szlavik Date: Tue, 8 Dec 2020 05:32:48 -0500 Subject: [PATCH 394/430] Retry tuya setup on auth rate limiting (#44001) Co-authored-by: Martin Hjelmare --- homeassistant/components/tuya/__init__.py | 5 +++++ homeassistant/components/tuya/config_flow.py | 14 ++++++++++++-- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/tuya/strings.json | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index bc665baeb86..5876331ea97 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -6,6 +6,7 @@ import logging from tuyaha import TuyaApi from tuyaha.tuyaapi import ( TuyaAPIException, + TuyaAPIRateLimitException, TuyaFrequentlyInvokeException, TuyaNetException, TuyaServerException, @@ -137,6 +138,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) as exc: raise ConfigEntryNotReady() from exc + except TuyaAPIRateLimitException as exc: + _LOGGER.error("Tuya login rate limited") + raise ConfigEntryNotReady() from exc + except TuyaAPIException as exc: _LOGGER.error( "Connection error during integration setup. Error: %s", diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index e2048aaf7bf..5d22a83e03e 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -2,7 +2,12 @@ import logging from tuyaha import TuyaApi -from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException +from tuyaha.tuyaapi import ( + TuyaAPIException, + TuyaAPIRateLimitException, + TuyaNetException, + TuyaServerException, +) import voluptuous as vol from homeassistant import config_entries @@ -103,7 +108,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): tuya.init( self._username, self._password, self._country_code, self._platform ) - except (TuyaNetException, TuyaServerException): + except (TuyaAPIRateLimitException, TuyaNetException, TuyaServerException): return RESULT_CONN_ERROR except TuyaAPIException: return RESULT_AUTH_FAILED @@ -249,6 +254,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + + if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + _LOGGER.error("Tuya integration not yet loaded") + return self.async_abort(reason="cannot_connect") + if user_input is not None: dev_ids = user_input.get(CONF_LIST_DEVICES) if dev_ids: diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 642a4dbe5d1..7481e56f00a 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuyaha==0.0.8"], + "requirements": ["tuyaha==0.0.9"], "codeowners": ["@ollo69"], "config_flow": true } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 84575906010..444ff0b5c21 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "step": { "init": { "title": "Configure Tuya Options", diff --git a/requirements_all.txt b/requirements_all.txt index 839a5bd0e39..4b204d1f95d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2207,7 +2207,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.8 +tuyaha==0.0.9 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1691ccdb263..0855d6fd616 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1069,7 +1069,7 @@ total_connect_client==0.55 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.8 +tuyaha==0.0.9 # homeassistant.components.twentemilieu twentemilieu==0.3.0 From a9d69bc1b95fb4663c36028651138204c6a829aa Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 7 Dec 2020 10:01:58 -0800 Subject: [PATCH 395/430] Bump pymyq to 2.0.11 (#44003) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 0e3d53be081..ee3471725b6 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.10"], + "requirements": ["pymyq==2.0.11"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 4b204d1f95d..d9c6096d5a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.10 +pymyq==2.0.11 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0855d6fd616..2edc59996f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.10 +pymyq==2.0.11 # homeassistant.components.nut pynut2==2.1.2 From e3b650b2c84a974f3c16471bebdb772d53919a83 Mon Sep 17 00:00:00 2001 From: JJdeVries <43748187+JJdeVries@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:46:53 +0100 Subject: [PATCH 396/430] Fix unit of measurement for asuswrt sensors (#44009) --- homeassistant/components/asuswrt/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 15ca58a525f..aa13bee81d0 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -29,7 +29,7 @@ class _SensorTypes(enum.Enum): UPLOAD_SPEED = "upload_speed" @property - def unit(self) -> Optional[str]: + def unit_of_measurement(self) -> Optional[str]: """Return a string with the unit of the sensortype.""" if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD): return DATA_GIGABYTES @@ -161,3 +161,8 @@ class AsuswrtSensor(CoordinatorEntity): def icon(self) -> Optional[str]: """Return the icon to use in the frontend.""" return self._type.icon + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + return self._type.unit_of_measurement From ac82dac4f3d622a6eb24ac55e8b537b95554bf7d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 7 Dec 2020 12:51:35 +0000 Subject: [PATCH 397/430] Hide HomeKit devices from discovery that are known to be problematic (#44014) --- .../components/homekit_controller/config_flow.py | 12 ++++++++++++ .../homekit_controller/test_config_flow.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9881ef15dcb..71c8005cbc5 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -21,6 +21,14 @@ HOMEKIT_BRIDGE_DOMAIN = "homekit" HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" +HOMEKIT_IGNORE = [ + # eufy Indoor Cam 2K Pan & Tilt + # https://github.com/home-assistant/core/issues/42307 + "T8410", + # Hive Hub - vendor does not give user a pairing code + "HHKBridge1,1", +] + PAIRING_FILE = "pairing.json" MDNS_SUFFIX = "._hap._tcp.local." @@ -255,6 +263,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. + if model in HOMEKIT_IGNORE: + return self.async_abort(reason="ignored_model") + + # If this is a HomeKit bridge exported by *this* HA instance ignore it. if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a8eb869abf4..72a8133159d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -257,6 +257,21 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info["properties"]["md"] = "HHKBridge1,1" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + +async def test_discovery_ignored_hk_bridge(hass, controller): + """Already paired.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") From 7a4dac81838d10af3fe596492aa9657fc40279e1 Mon Sep 17 00:00:00 2001 From: Fuzzy <16689090+FuzzyMistborn@users.noreply.github.com> Date: Tue, 8 Dec 2020 15:10:50 -0500 Subject: [PATCH 398/430] Add T8400 to ignore list (#44017) --- homeassistant/components/homekit_controller/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 71c8005cbc5..e046a131a6b 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -22,8 +22,9 @@ HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" HOMEKIT_IGNORE = [ - # eufy Indoor Cam 2K Pan & Tilt + # eufy Indoor Cam 2K and 2K Pan & Tilt # https://github.com/home-assistant/core/issues/42307 + "T8400", "T8410", # Hive Hub - vendor does not give user a pairing code "HHKBridge1,1", From b09a8f0d21974972a7645bb793213a9b6a024ecd Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 8 Dec 2020 17:01:07 +0000 Subject: [PATCH 399/430] Fix how homekit_controller enumerates Hue remote (#44019) --- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_hue_bridge.py | 19 +++++++++---------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 45c493ad864..9580a7ee50d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.57" + "aiohomekit==0.2.60" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index d9c6096d5a6..acb4eae7074 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2edc59996f9..4d1f812344b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http 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 67b7508eb94..168ae85b228 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -51,16 +51,15 @@ async def test_hue_bridge_setup(hass): ] for button in ("button1", "button2", "button3", "button4"): - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": button, - "subtype": subtype, - } - ) + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": "single_press", + } + ) triggers = await async_get_device_automations(hass, "trigger", device.id) assert_lists_same(triggers, expected) From 445fbb2c73dc7990e6318b25aabc518bac855395 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Dec 2020 13:34:47 -0700 Subject: [PATCH 400/430] Bump simplisafe-python to 9.6.1 (#44030) --- 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 0f209b366e8..eeb37b46df9 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.0"], + "requirements": ["simplisafe-python==9.6.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index acb4eae7074..c3725a7256a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2021,7 +2021,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.0 +simplisafe-python==9.6.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d1f812344b..f65478ad29e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.0 +simplisafe-python==9.6.1 # homeassistant.components.slack slackclient==2.5.0 From ec4b23173ca35fe919156263865eb81ed50d6238 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 8 Dec 2020 22:14:55 +0000 Subject: [PATCH 401/430] Update pyarlo to 0.2.4 (#44034) --- homeassistant/components/arlo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 41d4fc40e5f..f046f84f94d 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -2,7 +2,7 @@ "domain": "arlo", "name": "Arlo", "documentation": "https://www.home-assistant.io/integrations/arlo", - "requirements": ["pyarlo==0.2.3"], + "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c3725a7256a..b9b9074f975 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1271,7 +1271,7 @@ pyairvisual==5.0.4 pyalmond==0.0.2 # homeassistant.components.arlo -pyarlo==0.2.3 +pyarlo==0.2.4 # homeassistant.components.atag pyatag==0.3.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f65478ad29e..02677865bc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ pyairvisual==5.0.4 pyalmond==0.0.2 # homeassistant.components.arlo -pyarlo==0.2.3 +pyarlo==0.2.4 # homeassistant.components.atag pyatag==0.3.4.4 From 8ad0b9845e9338664ec1d833f6730c31245b65c9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Dec 2020 08:19:57 +0100 Subject: [PATCH 402/430] Add the missing ATTR_ENABLED attribute to Brother integration list of sensors (#44036) --- homeassistant/components/brother/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index cbb3d2a70cb..5aecde16327 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -136,6 +136,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", From 70257d7a211f3de94bad86e7dba63d73233f29de Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 7 Dec 2020 20:06:32 -0500 Subject: [PATCH 403/430] Update ZHA dependencies (#44039) zha-quirks==0.0.48 zigpy==0.28.2 zigpy-znp==0.3.0 --- homeassistant/components/zha/manifest.json | 7 ++++--- requirements_all.txt | 7 ++++--- requirements_test_all.txt | 10 +++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 780bb5bc999..f1821c9e480 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,13 +6,14 @@ "requirements": [ "bellows==0.21.0", "pyserial==3.4", - "zha-quirks==0.0.47", + "pyserial-asyncio==0.4", + "zha-quirks==0.0.48", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.28.1", + "zigpy==0.28.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.2.2" + "zigpy-znp==0.3.0" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index b9b9074f975..8b1c7d01827 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1661,6 +1661,7 @@ pysdcp==1 pysensibo==1.0.3 # homeassistant.components.serial +# homeassistant.components.zha pyserial-asyncio==0.4 # homeassistant.components.acer_projector @@ -2344,7 +2345,7 @@ zengge==0.2 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.47 +zha-quirks==0.0.48 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2365,10 +2366,10 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.2.2 +zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.1 +zigpy==0.28.2 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02677865bc2..4f2c56e3d3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -834,6 +834,10 @@ pyrisco==0.3.1 # homeassistant.components.ruckus_unleashed pyruckus==0.12 +# homeassistant.components.serial +# homeassistant.components.zha +pyserial-asyncio==0.4 + # homeassistant.components.acer_projector # homeassistant.components.zha pyserial==3.4 @@ -1140,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.47 +zha-quirks==0.0.48 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1155,7 +1159,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.2.2 +zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.1 +zigpy==0.28.2 From 5fee55f8245e9ced7521973ae08d21499d379003 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Dec 2020 19:57:19 -0700 Subject: [PATCH 404/430] Bump simplisafe-python to 9.6.2 (#44040) --- 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 eeb37b46df9..a502a7908f0 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.1"], + "requirements": ["simplisafe-python==9.6.2"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b1c7d01827..469e05be593 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2022,7 +2022,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.1 +simplisafe-python==9.6.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f2c56e3d3f..da4d91f22ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.1 +simplisafe-python==9.6.2 # homeassistant.components.slack slackclient==2.5.0 From a2f9cbc9415ad957de43c864a2720f0f33202f33 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Dec 2020 13:06:29 +0100 Subject: [PATCH 405/430] Fix extracting entity and device IDs from scripts (#44048) * Fix extracting entity and device IDs from scripts * Fix extracting from data_template --- homeassistant/helpers/script.py | 66 ++++++++++++------- tests/components/automation/test_init.py | 3 + .../blueprint/test_websocket_api.py | 2 +- tests/helpers/test_script.py | 41 +++++++++++- .../automation/test_event_service.yaml | 1 + 5 files changed, 86 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 645131b60b5..48a662e3a81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -22,10 +22,10 @@ from async_timeout import timeout import voluptuous as vol from homeassistant import exceptions -import homeassistant.components.device_automation as device_automation +from homeassistant.components import device_automation, scene from homeassistant.components.logger import LOGSEVERITY -import homeassistant.components.scene as scene from homeassistant.const import ( + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_ALIAS, CONF_CHOOSE, @@ -44,6 +44,7 @@ from homeassistant.const import ( CONF_REPEAT, CONF_SCENE, CONF_SEQUENCE, + CONF_TARGET, CONF_TIMEOUT, CONF_UNTIL, CONF_VARIABLES, @@ -60,13 +61,9 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.helpers import condition, config_validation as cv, template +from homeassistant.helpers import condition, config_validation as cv, service, template from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables -from homeassistant.helpers.service import ( - CONF_SERVICE_DATA, - async_prepare_call_from_config, -) from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -429,13 +426,13 @@ class _ScriptRun: self._script.last_action = self._action.get(CONF_ALIAS, "call service") self._log("Executing step %s", self._script.last_action) - domain, service, service_data = async_prepare_call_from_config( + domain, service_name, service_data = service.async_prepare_call_from_config( self._hass, self._action, self._variables ) running_script = ( domain == "automation" - and service == "trigger" + and service_name == "trigger" or domain in ("python_script", "script") ) # If this might start a script then disable the call timeout. @@ -448,7 +445,7 @@ class _ScriptRun: service_task = self._hass.async_create_task( self._hass.services.async_call( domain, - service, + service_name, service_data, blocking=True, context=self._context, @@ -755,6 +752,23 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[Dict[str, Any], MappingProxyType] +def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None: + """Extract referenced IDs.""" + if not data: + return + + item_ids = data.get(key) + + if item_ids is None or isinstance(item_ids, template.Template): + return + + if isinstance(item_ids, str): + item_ids = [item_ids] + + for item_id in item_ids: + found.add(item_id) + + class Script: """Representation of a script.""" @@ -889,7 +903,16 @@ class Script: for step in self.sequence: action = cv.determine_script_action(step) - if action == cv.SCRIPT_ACTION_CHECK_CONDITION: + if action == cv.SCRIPT_ACTION_CALL_SERVICE: + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) + + elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_devices(step) elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: @@ -910,20 +933,13 @@ class Script: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: - data = step.get(CONF_SERVICE_DATA) - if not data: - continue - - entity_ids = data.get(ATTR_ENTITY_ID) - - if entity_ids is None or isinstance(entity_ids, template.Template): - continue - - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - for entity_id in entity_ids: - referenced.add(entity_id) + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_entities(step) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 95667d9a690..5f258fc28b7 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1254,3 +1254,6 @@ async def test_blueprint_automation(hass, calls): hass.bus.async_fire("blueprint_event") await hass.async_block_till_done() assert len(calls) == 1 + assert automation.entities_in_automation(hass, "automation.automation_0") == [ + "light.kitchen" + ] diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index c4f39127d93..bb08414b6e8 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8f9e3cec36c..92666335f28 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1338,6 +1338,18 @@ async def test_referenced_entities(hass): "service": "test.script", "data": {"entity_id": "{{ 'light.service_template' }}"}, }, + { + "service": "test.script", + "entity_id": "light.direct_entity_referenced", + }, + { + "service": "test.script", + "target": {"entity_id": "light.entity_in_target"}, + }, + { + "service": "test.script", + "data_template": {"entity_id": "light.entity_in_data_template"}, + }, { "condition": "state", "entity_id": "sensor.condition", @@ -1357,6 +1369,9 @@ async def test_referenced_entities(hass): "light.service_list", "sensor.condition", "scene.hello", + "light.direct_entity_referenced", + "light.entity_in_target", + "light.entity_in_data_template", } # Test we cache results. assert script_obj.referenced_entities is script_obj.referenced_entities @@ -1374,12 +1389,36 @@ async def test_referenced_devices(hass): "device_id": "condition-dev-id", "domain": "switch", }, + { + "service": "test.script", + "data": {"device_id": "data-string-id"}, + }, + { + "service": "test.script", + "data_template": {"device_id": "data-template-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": "target-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": ["target-list-id-1", "target-list-id-2"]}, + }, ] ), "Test Name", "test_domain", ) - assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"} + assert script_obj.referenced_devices == { + "script-dev-id", + "condition-dev-id", + "data-string-id", + "data-template-string-id", + "target-string-id", + "target-list-id-1", + "target-list-id-2", + } # Test we cache results. assert script_obj.referenced_devices is script_obj.referenced_devices diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index eff8b52db16..ab067b004ac 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -9,3 +9,4 @@ trigger: event_type: !input trigger_event action: service: !input service_to_call + entity_id: light.kitchen From 88941eaa5148d8ca8c0cee02d543cd666191a960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 8 Dec 2020 14:16:31 +0100 Subject: [PATCH 406/430] Bump pyatv to 0.7.5 (#44051) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index ccd5da49547..21b2df308d3 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ - "pyatv==0.7.3" + "pyatv==0.7.5" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 469e05be593..e2d17cfe39a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ pyatmo==4.2.1 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.3 +pyatv==0.7.5 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da4d91f22ae..b086eb6c882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -649,7 +649,7 @@ pyatag==0.3.4.4 pyatmo==4.2.1 # homeassistant.components.apple_tv -pyatv==0.7.3 +pyatv==0.7.5 # homeassistant.components.blackbird pyblackbird==0.5 From 9cfeb44b565fe6f331cdaa58fe9647766b8faf06 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 8 Dec 2020 14:34:26 -0500 Subject: [PATCH 407/430] Exclude coordinator when looking up group members entity IDs (#44058) --- homeassistant/components/zha/core/group.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 8edb1da8f68..59277a394b3 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -194,6 +194,8 @@ class ZHAGroup(LogMixin): """Return entity ids from the entity domain for this group.""" domain_entity_ids: List[str] = [] for member in self.members: + if member.device.is_coordinator: + continue entities = async_entries_for_device( self._zha_gateway.ha_entity_registry, member.device.device_id, From 0e871c3390492c49d86523b659688f02d3baba13 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Dec 2020 19:02:44 +0100 Subject: [PATCH 408/430] Some lights only support hs, like the lidl christmas lights (#44059) --- homeassistant/components/deconz/light.py | 16 ++++++-- tests/components/deconz/test_light.py | 52 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index be967a76fea..6d759ccaf48 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -114,7 +114,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP - if self._device.xy is not None: + if self._device.xy is not None or ( + self._device.hue is not None and self._device.sat is not None + ): self._features |= SUPPORT_COLOR if self._device.effect is not None: @@ -141,8 +143,10 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def hs_color(self): """Return the hs color value.""" - if self._device.colormode in ("xy", "hs") and self._device.xy: - return color_util.color_xy_to_hs(*self._device.xy) + if self._device.colormode in ("xy", "hs"): + if self._device.xy: + return color_util.color_xy_to_hs(*self._device.xy) + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) return None @property @@ -163,7 +167,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + if self._device.xy is not None: + data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + else: + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) if ATTR_BRIGHTNESS in kwargs: data["bri"] = kwargs[ATTR_BRIGHTNESS] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index b971de28d43..18a135a5e05 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -353,3 +353,55 @@ async def test_configuration_tool(hass): await setup_deconz_integration(hass, get_state_response=data) assert len(hass.states.async_all()) == 0 + + +async def test_lidl_christmas_light(hass): + """Test that lights or groups entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = { + "0": { + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + xmas_light_device = gateway.api.lights["0"] + + assert len(hass.states.async_all()) == 1 + + with patch.object(xmas_light_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.xmas_light", + ATTR_HS_COLOR: (20, 30), + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", + "/lights/0/state", + json={"on": True, "hue": 3640, "sat": 76}, + ) + + assert hass.states.get("light.xmas_light") From 53b6a971d2330e42c945186c007ce240f7b2ae21 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Dec 2020 17:48:16 +0100 Subject: [PATCH 409/430] Fix ignored Axis config entries doesn't break set up of new entries (#44062) --- homeassistant/components/axis/config_flow.py | 3 ++- tests/components/axis/test_config_flow.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index ea1db54855b..8d52b7f8d9f 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -5,6 +5,7 @@ from ipaddress import ip_address import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -122,7 +123,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) - if entry.data[CONF_MODEL] == model + if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model ] name = model diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 24f888ea6ef..b9dceec7477 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.axis.const import ( DEFAULT_STREAM_PROFILE, DOMAIN as AXIS_DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -31,6 +31,8 @@ from tests.common import MockConfigEntry async def test_flow_manual_configuration(hass): """Test that config flow works.""" + MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} ) From f3eb21ba59da0cbe31f2b87fb601294bdc3cd246 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Dec 2020 19:08:29 +0100 Subject: [PATCH 410/430] Bumped version to 1.0.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8aa48b5178c..22e3a5f1813 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 29e3fbe568f880f10ba06b60965e36d741c94a99 Mon Sep 17 00:00:00 2001 From: zewelor Date: Thu, 10 Dec 2020 09:54:10 +0100 Subject: [PATCH 411/430] Fix yeelight unavailbility (#44061) --- homeassistant/components/yeelight/__init__.py | 66 ++++++++++++------- tests/components/yeelight/__init__.py | 3 +- tests/components/yeelight/test_init.py | 46 ++++++++++++- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index ae9d75de54f..324999c7124 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" +DEVICE_INITIALIZED = "yeelight_{}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 @@ -181,8 +181,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" async def _initialize(host: str, capabilities: Optional[dict] = None) -> None: - device = await _async_setup_device(hass, host, entry, capabilities) + async_dispatcher_connect( + hass, + DEVICE_INITIALIZED.format(host), + _load_platforms, + ) + + device = await _async_get_device(hass, host, entry, capabilities) hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device + + await device.async_setup() + + async def _load_platforms(): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -249,28 +260,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def _async_setup_device( - hass: HomeAssistant, - host: str, - entry: ConfigEntry, - capabilities: Optional[dict], -) -> None: - # Get model from config and capabilities - model = entry.options.get(CONF_MODEL) - if not model and capabilities is not None: - model = capabilities.get("model") - - # Set up device - bulb = Bulb(host, model=model or None) - if capabilities is None: - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) - - device = YeelightDevice(hass, host, entry.options, bulb, capabilities) - await hass.async_add_executor_job(device.update) - await device.async_setup() - return device - - @callback def _async_unique_name(capabilities: dict) -> str: """Generate name from capabilities.""" @@ -374,6 +363,7 @@ class YeelightDevice: self._device_type = None self._available = False self._remove_time_tracker = None + self._initialized = False self._name = host # Default name is host if capabilities: @@ -495,6 +485,8 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True + if not self._initialized: + self._initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -522,6 +514,11 @@ class YeelightDevice: ex, ) + def _initialize_device(self): + self._get_capabilities() + self._initialized = True + dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + def update(self): """Update device properties and send data updated signal.""" self._update_properties() @@ -584,3 +581,22 @@ class YeelightEntity(Entity): def update(self) -> None: """Update the entity.""" self._device.update() + + +async def _async_get_device( + hass: HomeAssistant, + host: str, + entry: ConfigEntry, + capabilities: Optional[dict], +) -> YeelightDevice: + # Get model from config and capabilities + model = entry.options.get(CONF_MODEL) + if not model and capabilities is not None: + model = capabilities.get("model") + + # Set up device + bulb = Bulb(host, model=model or None) + if capabilities is None: + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + + return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 9f811586a77..5405b69490b 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -55,7 +55,8 @@ PROPERTIES = { "current_brightness": "30", } -ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight" +ENTITY_BINARY_SENSOR_TEMPLATE = "binary_sensor.{}_nightlight" +ENTITY_BINARY_SENSOR = ENTITY_BINARY_SENSOR_TEMPLATE.format(UNIQUE_NAME) ENTITY_LIGHT = f"light.{UNIQUE_NAME}" ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight" ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index d9c23cfa1a7..882f9944ca1 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,21 +1,27 @@ """Test Yeelight.""" +from unittest.mock import MagicMock + from yeelight import BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, + DATA_CONFIG_ENTRIES, + DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from . import ( + CAPABILITIES, CONFIG_ENTRY_DATA, ENTITY_AMBILIGHT, ENTITY_BINARY_SENSOR, + ENTITY_BINARY_SENSOR_TEMPLATE, ENTITY_LIGHT, ENTITY_NIGHTLIGHT, ID, @@ -115,6 +121,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -132,3 +139,40 @@ async def test_unique_ids_entry(hass: HomeAssistant): assert ( er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight" ) + + +async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): + """Test Yeelight off while adding to ha, for example on HA start.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + **CONFIG_ENTRY_DATA, + CONF_HOST: IP_ADDRESS, + }, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( + IP_ADDRESS.replace(".", "_") + ) + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is None + + type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) + type(mocked_bulb).get_properties = MagicMock(None) + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.async_block_till_done() + + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is not None From 6ea3c671e9b85d9779d5f394c5e6de551c1b501e Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Thu, 10 Dec 2020 01:09:08 -0800 Subject: [PATCH 412/430] Fix transmission torrent filtering and sorting (#44069) --- homeassistant/components/transmission/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ef1e68e2d0a..ea62de71e8d 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -145,12 +145,10 @@ class TransmissionTorrentsSensor(TransmissionSensor): @property def device_state_attributes(self): """Return the state attributes, if any.""" - limit = self._tm_client.config_entry.options[CONF_LIMIT] - order = self._tm_client.config_entry.options[CONF_ORDER] - torrents = self._tm_client.api.torrents[0:limit] info = _torrents_info( - torrents, - order=order, + torrents=self._tm_client.api.torrents, + order=self._tm_client.config_entry.options[CONF_ORDER], + limit=self._tm_client.config_entry.options[CONF_LIMIT], statuses=self.SUBTYPE_MODES[self._sub_type], ) return { @@ -173,11 +171,11 @@ def _filter_torrents(torrents, statuses=None): ] -def _torrents_info(torrents, order, statuses=None): +def _torrents_info(torrents, order, limit, statuses=None): infos = {} torrents = _filter_torrents(torrents, statuses) torrents = SUPPORTED_ORDER_MODES[order](torrents) - for torrent in _filter_torrents(torrents, statuses): + for torrent in torrents[:limit]: info = infos[torrent.name] = { "added_date": torrent.addedDate, "percent_done": f"{torrent.percentDone * 100:.2f}", From e68544cd7dfcc88bd6e9c41d7591ecc941be48fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 10:10:38 +0100 Subject: [PATCH 413/430] Bump hass-nabucasa to 0.39.0 (#44097) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 3f65ed2ba46..03bf2761857 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.38.0"], + "requirements": ["hass-nabucasa==0.39.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d820f5e715b..61d8d4a35a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==3.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' diff --git a/requirements_all.txt b/requirements_all.txt index e2d17cfe39a..d020d105625 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b086eb6c882..7cc159f9212 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,7 @@ ha-ffmpeg==3.0.2 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 # homeassistant.components.tasmota hatasmota==0.1.4 From cff4f7bbbfd068f550cc7808149d80c83b1e5744 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 09:12:20 +0000 Subject: [PATCH 414/430] Bumped version to 1.0.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 22e3a5f1813..72f422ed8fb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From a01cf72d673d558bd8dcd42a8df5d4a33fc03fd5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 11:11:49 +0100 Subject: [PATCH 415/430] Fix importing blueprints from forums with HTML entities (#44098) --- .../components/blueprint/importer.py | 3 +- tests/components/blueprint/test_importer.py | 79 +- tests/fixtures/blueprint/community_post.json | 749 ++++++++---------- 3 files changed, 391 insertions(+), 440 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 524b04293ee..f0230aba1b7 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -1,5 +1,6 @@ """Import logic for blueprint.""" from dataclasses import dataclass +import html import re from typing import Optional @@ -110,7 +111,7 @@ def _extract_blueprint_from_community_topic( block_content = block_content.strip() try: - data = yaml.parse_yaml(block_content) + data = yaml.parse_yaml(html.unescape(block_content)) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index bb8903459c9..8e674e3a9de 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -16,6 +16,70 @@ def community_post(): return load_fixture("blueprint/community_post.json") +COMMUNITY_POST_INPUTS = { + "remote": { + "name": "Remote", + "description": "IKEA remote to use", + "selector": { + "device": { + "integration": "zha", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + } + }, + }, + "light": { + "name": "Light(s)", + "description": "The light(s) to control", + "selector": {"target": {"entity": {"domain": "light"}}}, + }, + "force_brightness": { + "name": "Force turn on brightness", + "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n', + "default": False, + "selector": {"boolean": {}}, + }, + "brightness": { + "name": "Brightness", + "description": "Brightness of the light(s) when turning on", + "default": 50, + "selector": { + "number": { + "min": 0.0, + "max": 100.0, + "mode": "slider", + "step": 1.0, + "unit_of_measurement": "%", + } + }, + }, + "button_left_short": { + "name": "Left button - short press", + "description": "Action to run on short left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_left_long": { + "name": "Left button - long press", + "description": "Action to run on long left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_short": { + "name": "Right button - short press", + "description": "Action to run on short right button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_long": { + "name": "Right button - long press", + "description": "Action to run on long right button press", + "default": [], + "selector": {"action": {}}, + }, +} + + def test_get_community_post_import_url(): """Test variations of generating import forum url.""" assert ( @@ -57,10 +121,7 @@ def test_extract_blueprint_from_community_topic(community_post): ) assert imported_blueprint is not None assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS def test_extract_blueprint_from_community_topic_invalid_yaml(): @@ -103,11 +164,11 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit ) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } - assert imported_blueprint.suggested_filename == "balloob/test-topic" + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS + assert ( + imported_blueprint.suggested_filename + == "frenck/zha-ikea-five-button-remote-for-lights" + ) assert ( imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json index 5b9a3dcb9c7..121d53ad94e 100644 --- a/tests/fixtures/blueprint/community_post.json +++ b/tests/fixtures/blueprint/community_post.json @@ -2,39 +2,58 @@ "post_stream": { "posts": [ { - "id": 1144853, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:12.688Z", - "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e", + "id": 1216212, + "name": "Franck Nijhof", + "username": "frenck", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "created_at": "2020-12-10T09:20:58.974Z", + "cooked": "\u003cp\u003eThis is a blueprint for the IKEA five-button remotes (the round ones), specifically for use with ZHA.\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg\" data-download-href=\"/uploads/short-url/8SdGCUtkzOTNpMjggpBvSFs4WQ.jpeg?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg\" alt=\"image\" data-base62-sha1=\"8SdGCUtkzOTNpMjggpBvSFs4WQ\" width=\"500\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_750x750.jpeg 1.5x, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_1000x1000.jpeg 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e1400×1400 150 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eIt was specially created for use with (any) light(s). As the basic light controls are already mapped in this blueprint.\u003c/p\u003e\n\u003cp\u003eThe middle “on” button, toggle the lights on/off to the last set brightness (unless the force brightness is toggled on in the blueprint). Dim up/down buttons will change the brightness smoothly and can be pressed and hold until the brightness is satisfactory.\u003c/p\u003e\n\u003cp\u003eThe “left” and “right” buttons can be assigned to a short and long button press action. This allows you to assign, e.g., a scene or anything else.\u003c/p\u003e\n\u003cp\u003eThis is what the Blueprint looks like from the UI:\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png\" data-download-href=\"/uploads/short-url/mf5vhlKYe6yeuFayUzlCTBfveKf.png?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png\" alt=\"image\" data-base62-sha1=\"mf5vhlKYe6yeuFayUzlCTBfveKf\" width=\"610\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png, https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_915x750.png 1.5x, https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e975×799 64.1 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eBlueprint, which you can import by using this forum topic URL:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003eblueprint:\n name: ZHA - IKEA five button remote for lights\n description: |\n Control lights with an IKEA five button remote (the round ones).\n\n The middle \"on\" button, toggle the lights on/off to the last set brightness\n (unless the force brightness is toggled on in the blueprint).\n\n Dim up/down buttons will change the brightness smoothly and can be pressed\n and hold until the brightness is satisfactory.\n\n The \"left\" and \"right\" buttons can be assigned to a short and long button\n press action. This allows you to assign, e.g., a scene or anything else.\n\n domain: automation\n input:\n remote:\n name: Remote\n description: IKEA remote to use\n selector:\n device:\n integration: zha\n manufacturer: IKEA of Sweden\n model: TRADFRI remote control\n light:\n name: Light(s)\n description: The light(s) to control\n selector:\n target:\n entity:\n domain: light\n force_brightness:\n name: Force turn on brightness\n description: \u0026gt;\n Force the brightness to the set level below, when the \"on\" button on\n the remote is pushed and lights turn on.\n default: false\n selector:\n boolean:\n brightness:\n name: Brightness\n description: Brightness of the light(s) when turning on\n default: 50\n selector:\n number:\n min: 0\n max: 100\n mode: slider\n step: 1\n unit_of_measurement: \"%\"\n button_left_short:\n name: Left button - short press\n description: Action to run on short left button press\n default: []\n selector:\n action:\n button_left_long:\n name: Left button - long press\n description: Action to run on long left button press\n default: []\n selector:\n action:\n button_right_short:\n name: Right button - short press\n description: Action to run on short right button press\n default: []\n selector:\n action:\n button_right_long:\n name: Right button - long press\n description: Action to run on long right button press\n default: []\n selector:\n action:\n\nmode: restart\nmax_exceeded: silent\n\nvariables:\n force_brightness: !input force_brightness\n\ntrigger:\n - platform: event\n event_type: zha_event\n event_data:\n device_id: !input remote\n\naction:\n - variables:\n command: \"{{ trigger.event.data.command }}\"\n cluster_id: \"{{ trigger.event.data.cluster_id }}\"\n endpoint_id: \"{{ trigger.event.data.endpoint_id }}\"\n args: \"{{ trigger.event.data.args }}\"\n - choose:\n - conditions:\n - \"{{ command == 'toggle' }}\"\n - \"{{ cluster_id == 6 }}\"\n - \"{{ endpoint_id == 1 }}\"\n sequence:\n - choose:\n - conditions: \"{{ force_brightness }}\"\n sequence:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n brightness_pct: !input brightness\n default:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n\n - conditions:\n - \"{{ command == 'step_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'step' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [257, 13, 0] }}\"\n sequence: !input button_left_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3329, 0] }}\"\n sequence: !input button_left_long\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [256, 13, 0] }}\"\n sequence: !input button_right_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3328, 0] }}\"\n sequence: !input button_right_long\n\u003c/code\u003e\u003c/pre\u003e", "post_number": 1, "post_type": 1, - "updated_at": "2020-10-20T08:24:14.189Z", + "updated_at": "2020-12-10T09:22:08.993Z", "reply_count": 0, "reply_to_post_number": null, "quote_count": 0, "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", + "reads": 3, + "readers_count": 2, + "score": 0.6, + "yours": false, + "topic_id": 253804, + "topic_slug": "zha-ikea-five-button-remote-for-lights", + "display_username": "Franck Nijhof", "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_bg_color": null, "primary_group_flair_color": null, - "version": 2, + "version": 1, "can_edit": true, "can_delete": false, "can_recover": false, "can_wiki": true, + "link_counts": [ + { + "url": "https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "internal": false, + "reflection": false, + "title": "9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "clicks": 0 + }, + { + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "internal": false, + "reflection": false, + "title": "0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "clicks": 0 + } + ], "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, + "user_title": null, "actions_summary": [ + { + "id": 2, + "can_act": true + }, { "id": 3, "can_act": true @@ -48,75 +67,7 @@ "can_act": true }, { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144854, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:17.535Z", - "cooked": "", - "post_number": 2, - "post_type": 3, - "updated_at": "2020-10-16T12:20:17.535Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 1, - "reads": 2, - "readers_count": 1, - "score": 5.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, + "id": 6, "can_act": true }, { @@ -127,82 +78,9 @@ "moderator": true, "admin": true, "staff": true, - "user_id": 3, + "user_id": 10250, "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "action_code": "visible.disabled", - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144872, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:27:53.926Z", - "cooked": "\u003cp\u003eTest reply!\u003c/p\u003e", - "post_number": 3, - "post_type": 1, - "updated_at": "2020-10-16T12:27:53.926Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, - "can_act": true - }, - { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, + "trust_level": 4, "deleted_at": null, "user_deleted": false, "edit_reason": null, @@ -211,7 +89,7 @@ "reviewable_id": 0, "reviewable_score_count": 0, "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", + "user_created_at": "2017-08-12T12:46:55.467Z", "user_date_of_birth": null, "user_signature": null, "can_accept_answer": false, @@ -220,36 +98,34 @@ } ], "stream": [ - 1144853, - 1144854, - 1144872 + 1216212 ] }, "timeline_lookup": [ [ 1, - 3 + 0 ] ], "suggested_topics": [ { - "id": 17750, - "title": "Tutorial: Creating your first add-on", - "fancy_title": "Tutorial: Creating your first add-on", - "slug": "tutorial-creating-your-first-add-on", - "posts_count": 26, - "reply_count": 14, - "highest_post_number": 27, - "image_url": null, - "created_at": "2017-05-14T07:51:33.946Z", - "last_posted_at": "2020-07-28T11:29:27.892Z", + "id": 168593, + "title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "fancy_title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "slug": "dwains-dashboard-1-click-install-lovelace-dashboard-for-desktop-tablet-and-mobile-v2-0-0", + "posts_count": 1162, + "reply_count": 785, + "highest_post_number": 1185, + "image_url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg", + "created_at": "2020-02-03T13:15:24.364Z", + "last_posted_at": "2020-12-10T07:57:47.304Z", "bumped": true, - "bumped_at": "2020-07-28T11:29:27.892Z", + "bumped_at": "2020-12-10T07:57:47.304Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 18, - "unread": 7, - "new_posts": 2, + "last_read_post_number": 81, + "unread": 0, + "new_posts": 1109, "pinned": false, "unpinned": null, "visible": true, @@ -258,11 +134,19 @@ "notification_level": 2, "bookmarked": false, "liked": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 296, + "height": 50, + "url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg" + } + ], "tags": [], - "like_count": 9, - "views": 4355, - "category_id": 25, + "like_count": 1214, + "views": 71580, + "category_id": 34, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -270,50 +154,50 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 36674, + "username": "dwains", + "name": "Dwain Scheeren", + "avatar_template": "/user_avatar/community.home-assistant.io/dwains/{size}/100261_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9852, - "username": "JSCSJSCS", + "id": 16514, + "username": "jimpower", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/jscsjscs/{size}/38256_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/jimpower/{size}/66909_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 11494, - "username": "so3n", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/so3n/{size}/46007_2.png" + "id": 1473, + "username": "thundergreen", + "name": "Thundergreen", + "avatar_template": "/user_avatar/community.home-assistant.io/thundergreen/{size}/18379_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9094, - "username": "IoTnerd", - "name": "Balázs Suhajda", - "avatar_template": "/user_avatar/community.home-assistant.io/iotnerd/{size}/33526_2.png" + "id": 64369, + "username": "MRobi", + "name": "Mike", + "avatar_template": "/user_avatar/community.home-assistant.io/mrobi/{size}/113127_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 73134, - "username": "diord", - "name": "", - "avatar_template": "/letter_avatar/diord/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 9646, + "username": "Freshhat", + "name": "Freshhat", + "avatar_template": "/user_avatar/community.home-assistant.io/freshhat/{size}/24797_2.png" } } ] @@ -323,19 +207,19 @@ "title": "Lovelace: Button card", "fancy_title": "Lovelace: Button card", "slug": "lovelace-button-card", - "posts_count": 4608, - "reply_count": 3522, - "highest_post_number": 4691, + "posts_count": 4775, + "reply_count": 3635, + "highest_post_number": 4858, "image_url": null, "created_at": "2018-08-28T00:18:19.312Z", - "last_posted_at": "2020-10-20T07:33:29.523Z", + "last_posted_at": "2020-12-10T04:42:58.851Z", "bumped": true, - "bumped_at": "2020-10-20T07:33:29.523Z", + "bumped_at": "2020-12-10T04:42:58.851Z", "archetype": "regular", "unseen": false, "last_read_post_number": 1938, "unread": 369, - "new_posts": 2384, + "new_posts": 2551, "pinned": false, "unpinned": null, "visible": true, @@ -346,8 +230,8 @@ "liked": false, "thumbnails": null, "tags": [], - "like_count": 1700, - "views": 184752, + "like_count": 1740, + "views": 199965, "category_id": 34, "featured_link": null, "has_accepted_answer": false, @@ -366,20 +250,20 @@ "extras": null, "description": "Frequent Poster", "user": { - "id": 2019, - "username": "iantrich", - "name": "Ian", - "avatar_template": "/user_avatar/community.home-assistant.io/iantrich/{size}/154042_2.png" + "id": 33228, + "username": "jimz011", + "name": "Jim", + "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 33228, - "username": "jimz011", - "name": "Jim", - "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" + "id": 12475, + "username": "Mariusthvdb", + "name": "Marius", + "avatar_template": "/user_avatar/community.home-assistant.io/mariusthvdb/{size}/49008_2.png" } }, { @@ -396,32 +280,32 @@ "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 26227, - "username": "RomRider", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/romrider/{size}/41384_2.png" + "id": 52090, + "username": "parautenbach", + "name": "Pieter Rautenbach", + "avatar_template": "/user_avatar/community.home-assistant.io/parautenbach/{size}/89345_2.png" } } ] }, { - "id": 10564, - "title": "Professional/Commercial Use?", - "fancy_title": "Professional/Commercial Use?", - "slug": "professional-commercial-use", - "posts_count": 54, - "reply_count": 37, - "highest_post_number": 54, + "id": 58639, + "title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "fancy_title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "slug": "echo-devices-alexa-as-media-player-testers-needed", + "posts_count": 4429, + "reply_count": 3009, + "highest_post_number": 4517, "image_url": null, - "created_at": "2017-01-27T05:01:57.453Z", - "last_posted_at": "2020-10-20T07:03:57.895Z", + "created_at": "2018-07-04T03:36:22.187Z", + "last_posted_at": "2020-12-10T04:26:11.298Z", "bumped": true, - "bumped_at": "2020-10-20T07:03:57.895Z", + "bumped_at": "2020-12-10T04:26:11.298Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, + "last_read_post_number": 3219, "unread": 0, - "new_posts": 47, + "new_posts": 1298, "pinned": false, "unpinned": null, "visible": true, @@ -431,104 +315,12 @@ "bookmarked": false, "liked": false, "thumbnails": null, - "tags": [], - "like_count": 21, - "views": 10695, - "category_id": 17, - "featured_link": null, - "has_accepted_answer": false, - "posters": [ - { - "extras": null, - "description": "Original Poster", - "user": { - "id": 4758, - "username": "oobie11", - "name": "Bryan", - "avatar_template": "/user_avatar/community.home-assistant.io/oobie11/{size}/37858_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 18386, - "username": "pitp2", - "name": "", - "avatar_template": "/letter_avatar/pitp2/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 23116, - "username": "jortegamx", - "name": "Jake", - "avatar_template": "/user_avatar/community.home-assistant.io/jortegamx/{size}/45515_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 39038, - "username": "orif73", - "name": "orif73", - "avatar_template": "/letter_avatar/orif73/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": "latest", - "description": "Most Recent Poster", - "user": { - "id": 41040, - "username": "devastator", - "name": "", - "avatar_template": "/letter_avatar/devastator/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - } - ] - }, - { - "id": 219480, - "title": "What the heck is with the 'latest state change' not being kept after restart?", - "fancy_title": "What the heck is with the \u0026lsquo;latest state change\u0026rsquo; not being kept after restart?", - "slug": "what-the-heck-is-with-the-latest-state-change-not-being-kept-after-restart", - "posts_count": 37, - "reply_count": 13, - "highest_post_number": 38, - "image_url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png", - "created_at": "2020-08-18T13:10:09.367Z", - "last_posted_at": "2020-10-20T00:32:07.312Z", - "bumped": true, - "bumped_at": "2020-10-20T00:32:07.312Z", - "archetype": "regular", - "unseen": false, - "last_read_post_number": 8, - "unread": 0, - "new_posts": 30, - "pinned": false, - "unpinned": null, - "visible": true, - "closed": false, - "archived": false, - "notification_level": 2, - "bookmarked": false, - "liked": false, - "thumbnails": [ - { - "max_width": null, - "max_height": null, - "width": 469, - "height": 59, - "url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png" - } + "tags": [ + "alexa" ], - "tags": [], - "like_count": 26, - "views": 1722, - "category_id": 52, + "like_count": 1092, + "views": 179580, + "category_id": 47, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -536,72 +328,72 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3124, - "username": "andriej", + "id": 1084, + "username": "keatontaylor", + "name": "Keatontaylor", + "avatar_template": "/letter_avatar/keatontaylor/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 24884, + "username": "h4nc", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/andriej/{size}/24457_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/h4nc/{size}/68244_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 15052, - "username": "Misiu", + "id": 9191, + "username": "finity", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/misiu/{size}/20752_2.png" + "avatar_template": "/letter_avatar/finity/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 4629, - "username": "lolouk44", - "name": "lolouk44", - "avatar_template": "/user_avatar/community.home-assistant.io/lolouk44/{size}/119845_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 51736, - "username": "hmoffatt", - "name": "Hamish Moffatt", - "avatar_template": "/user_avatar/community.home-assistant.io/hmoffatt/{size}/88700_2.png" + "id": 1269, + "username": "ReneTode", + "name": "", + "avatar_template": "/user_avatar/community.home-assistant.io/renetode/{size}/1533_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 78711, - "username": "Astrosteve", - "name": "Steve", - "avatar_template": "/letter_avatar/astrosteve/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 46136, + "username": "chirad", + "name": "Dinoj", + "avatar_template": "/letter_avatar/chirad/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } } ] }, { - "id": 162594, - "title": "A different take on designing a Lovelace UI", - "fancy_title": "A different take on designing a Lovelace UI", - "slug": "a-different-take-on-designing-a-lovelace-ui", - "posts_count": 641, - "reply_count": 425, - "highest_post_number": 654, + "id": 252336, + "title": "Unhealthy state", + "fancy_title": "Unhealthy state", + "slug": "unhealthy-state", + "posts_count": 89, + "reply_count": 69, + "highest_post_number": 91, "image_url": null, - "created_at": "2020-01-11T23:09:25.207Z", - "last_posted_at": "2020-10-19T23:32:15.555Z", + "created_at": "2020-12-05T20:32:00.864Z", + "last_posted_at": "2020-12-09T22:41:30.212Z", "bumped": true, - "bumped_at": "2020-10-19T23:32:15.555Z", + "bumped_at": "2020-12-09T22:41:30.212Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, - "unread": 32, - "new_posts": 615, + "last_read_post_number": 75, + "unread": 0, + "new_posts": 16, "pinned": false, "unpinned": null, "visible": true, @@ -609,12 +401,12 @@ "archived": false, "notification_level": 2, "bookmarked": false, - "liked": false, + "liked": true, "thumbnails": null, "tags": [], - "like_count": 453, - "views": 68547, - "category_id": 9, + "like_count": 33, + "views": 946, + "category_id": 11, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -622,90 +414,179 @@ "extras": null, "description": "Original Poster", "user": { - "id": 11256, - "username": "Mattias_Persson", - "name": "Mattias Persson", - "avatar_template": "/user_avatar/community.home-assistant.io/mattias_persson/{size}/14773_2.png" + "id": 26121, + "username": "helgemor", + "name": "Helge", + "avatar_template": "/user_avatar/community.home-assistant.io/helgemor/{size}/42574_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 27634, - "username": "Jason_hill", - "name": "Jason Hill", - "avatar_template": "/user_avatar/community.home-assistant.io/jason_hill/{size}/93218_2.png" + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46782, - "username": "Martin_Pejstrup", - "name": "mpejstrup", - "avatar_template": "/user_avatar/community.home-assistant.io/martin_pejstrup/{size}/78412_2.png" + "id": 28146, + "username": "123", + "name": "Taras", + "avatar_template": "/user_avatar/community.home-assistant.io/123/{size}/44349_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46841, - "username": "spudje", - "name": "", - "avatar_template": "/letter_avatar/spudje/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 8361, + "username": "kanga_who", + "name": "Jason", + "avatar_template": "/user_avatar/community.home-assistant.io/kanga_who/{size}/46427_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 20924, - "username": "Diego_Santos", - "name": "Diego Santos", - "avatar_template": "/user_avatar/community.home-assistant.io/diego_santos/{size}/29096_2.png" + "id": 44704, + "username": "joselito1", + "name": "jose litomans", + "avatar_template": "/user_avatar/community.home-assistant.io/joselito1/{size}/75914_2.png" + } + } + ] + }, + { + "id": 130280, + "title": "Home Assistant Cast", + "fancy_title": "Home Assistant Cast", + "slug": "home-assistant-cast", + "posts_count": 282, + "reply_count": 206, + "highest_post_number": 289, + "image_url": null, + "created_at": "2019-08-06T15:59:00.183Z", + "last_posted_at": "2020-12-09T16:48:51.132Z", + "bumped": true, + "bumped_at": "2020-12-09T16:48:51.132Z", + "archetype": "regular", + "unseen": false, + "last_read_post_number": 88, + "unread": 0, + "new_posts": 201, + "pinned": false, + "unpinned": null, + "visible": true, + "closed": false, + "archived": false, + "notification_level": 3, + "bookmarked": false, + "liked": false, + "thumbnails": null, + "tags": [], + "like_count": 94, + "views": 29308, + "category_id": 30, + "featured_link": null, + "has_accepted_answer": false, + "posters": [ + { + "extras": null, + "description": "Original Poster", + "user": { + "id": -1, + "username": "system", + "name": "system", + "avatar_template": "/user_avatar/community.home-assistant.io/system/{size}/13_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 11649, + "username": "DavidFW1960", + "name": "David", + "avatar_template": "/user_avatar/community.home-assistant.io/davidfw1960/{size}/66886_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 26084, + "username": "Yoinkz", + "name": "", + "avatar_template": "/letter_avatar/yoinkz/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" + } + }, + { + "extras": "latest", + "description": "Most Recent Poster", + "user": { + "id": 45396, + "username": "Wetzel402", + "name": "Cody", + "avatar_template": "/user_avatar/community.home-assistant.io/wetzel402/{size}/76694_2.png" } } ] } ], - "tags": [], - "id": 236133, - "title": "Test Topic", - "fancy_title": "Test Topic", - "posts_count": 3, - "created_at": "2020-10-16T12:20:12.580Z", - "views": 13, + "tags": [ + "blueprint", + "zha" + ], + "id": 253804, + "title": "ZHA - IKEA five button remote for lights", + "fancy_title": "ZHA - IKEA five button remote for lights", + "posts_count": 1, + "created_at": "2020-12-10T09:20:58.681Z", + "views": 4, "reply_count": 0, "like_count": 0, - "last_posted_at": "2020-10-16T12:27:53.926Z", - "visible": false, + "last_posted_at": "2020-12-10T09:20:58.974Z", + "visible": true, "closed": false, "archived": false, "has_summary": false, "archetype": "regular", - "slug": "test-topic", - "category_id": 1, - "word_count": 37, + "slug": "zha-ikea-five-button-remote-for-lights", + "category_id": 53, + "word_count": 633, "deleted_at": null, - "user_id": 3, + "user_id": 10250, "featured_link": null, "pinned_globally": false, "pinned_at": null, "pinned_until": null, - "image_url": null, + "image_url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", "draft": null, - "draft_key": "topic_236133", - "draft_sequence": 8, - "posted": true, + "draft_key": "topic_253804", + "draft_sequence": 0, + "posted": false, "unpinned": null, "pinned": false, "current_post_number": 1, - "highest_post_number": 3, - "last_read_post_number": 3, - "last_read_post_id": 1144872, + "highest_post_number": 1, + "last_read_post_number": 1, + "last_read_post_id": 1216212, "deleted_by": null, "has_deleted": false, "actions_summary": [ @@ -732,16 +613,24 @@ "bookmarked": false, "topic_timer": null, "private_topic_timer": null, - "message_bus_last_id": 5, + "message_bus_last_id": 4, "participant_count": 1, "show_read_indicator": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 1400, + "height": 1400, + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg" + } + ], "can_vote": false, "vote_count": null, "user_voted": false, "details": { - "notification_level": 3, - "notifications_reason_id": 1, + "notification_level": 1, + "notifications_reason_id": null, "can_move_posts": true, "can_edit": true, "can_delete": true, @@ -756,11 +645,11 @@ "can_remove_self_id": 3, "participants": [ { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "post_count": 3, + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "post_count": 1, "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_color": null, @@ -768,16 +657,16 @@ } ], "created_by": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" }, "last_poster": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" } } } From 83b7439a0819d4464b4c08243c33eae728f074bf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Dec 2020 16:45:57 +0100 Subject: [PATCH 416/430] Fix importing blueprint from community (#44104) --- homeassistant/components/blueprint/importer.py | 4 ++-- tests/components/blueprint/test_importer.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index f0230aba1b7..217851df980 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -108,10 +108,10 @@ def _extract_blueprint_from_community_topic( if block_syntax not in ("auto", "yaml"): continue - block_content = block_content.strip() + block_content = html.unescape(block_content.strip()) try: - data = yaml.parse_yaml(html.unescape(block_content)) + data = yaml.parse_yaml(block_content) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 8e674e3a9de..382363aa560 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -173,6 +173,7 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" ) + assert "gt;" not in imported_blueprint.raw_data @pytest.mark.parametrize( From 42d1644762f34e85a97415abcabdb087b6eb13bb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Dec 2020 17:54:55 +0100 Subject: [PATCH 417/430] Update frontend to 20201210.0 (#44105) --- 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 65fe745e51d..cb52afd4b65 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201204.0"], + "requirements": ["home-assistant-frontend==20201210.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 61d8d4a35a6..a08b3904b72 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index d020d105625..90f5c6267a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cc159f9212..766965f52f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3986df63dc3cfd118d0d2d82694829303aab08b8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Dec 2020 21:25:50 +0100 Subject: [PATCH 418/430] Support more errors to better do retries in UniFi (#44108) --- homeassistant/components/unifi/controller.py | 14 ++++++++++++-- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 16 ++++++++++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index a4435ccfecf..30b82c65c85 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -397,7 +397,12 @@ class UniFiController: await self.api.login() self.api.start_websocket() - except (asyncio.TimeoutError, aiounifi.AiounifiException): + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.AiounifiException, + ): self.hass.loop.call_later(RETRY_TIMER, self.reconnect) @callback @@ -464,7 +469,12 @@ async def get_controller( LOGGER.warning("Connected to UniFi at %s but not registered.", host) raise AuthenticationRequired from err - except (asyncio.TimeoutError, aiounifi.RequestError) as err: + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.RequestError, + ) as err: LOGGER.error("Error connecting to the UniFi controller at %s", host) raise CannotConnect from err diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 48c080f82f7..94b1c90f4f3 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==25"], + "requirements": ["aiounifi==26"], "codeowners": ["@Kane610"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 90f5c6267a2..2d7b7788db5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 766965f52f3..dd704b759b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 83732601cd6..8d5cb85bf9f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -295,6 +295,22 @@ async def test_get_controller_login_failed(hass): await get_controller(hass, **CONTROLLER_DATA) +async def test_get_controller_controller_bad_gateway(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.BadGateway + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + +async def test_get_controller_controller_service_unavailable(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.ServiceUnavailable + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( From 9c6ff9af91a85fb9d6f7d21494db2a63c22b1b63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Dec 2020 21:34:52 +0100 Subject: [PATCH 419/430] Bumped version to 1.0.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 72f422ed8fb..b1a196fe232 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 8c6636994ffb3365325d60a7c988c67f0bf1f9ca Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Fri, 11 Dec 2020 10:53:21 -0500 Subject: [PATCH 420/430] Fix Met.no forecast precipitation (#44106) --- homeassistant/components/met/weather.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 73b9134415d..c0c8c11c644 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -21,8 +21,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS, @@ -32,7 +34,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure -from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP +from .const import ( + ATTR_FORECAST_PRECIPITATION, + ATTR_MAP, + CONDITIONS_MAP, + CONF_TRACK_HOME, + DOMAIN, + FORECAST_MAP, +) _LOGGER = logging.getLogger(__name__) @@ -221,6 +230,14 @@ class MetWeather(CoordinatorEntity, WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } + if not self._is_metric: + if ATTR_FORECAST_PRECIPITATION in ha_item: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) + ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 8507b620168ae90bc3c6edf70544cd1ead76da51 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 12 Dec 2020 02:43:38 -0700 Subject: [PATCH 421/430] Fix inability to erase SimpliSafe code (#44137) --- .../components/simplisafe/config_flow.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index c34255bc62a..f17a2ce2e4c 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -15,6 +15,15 @@ from homeassistant.helpers import aiohttp_client from . import async_get_client_id from .const import DOMAIN, LOGGER # pylint: disable=unused-import +FULL_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CODE): str, + } +) +PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -24,15 +33,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.full_data_schema = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CODE): str, - } - ) - self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str}) - self._code = None self._password = None self._username = None @@ -125,21 +125,19 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle re-auth completion.""" if not user_input: return self.async_show_form( - step_id="reauth_confirm", data_schema=self.password_data_schema + step_id="reauth_confirm", data_schema=PASSWORD_DATA_SCHEMA ) self._password = user_input[CONF_PASSWORD] return await self._async_login_during_step( - step_id="reauth_confirm", form_schema=self.password_data_schema + step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA ) 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=self.full_data_schema - ) + return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() @@ -149,7 +147,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] return await self._async_login_during_step( - step_id="user", form_schema=self.full_data_schema + step_id="user", form_schema=FULL_DATA_SCHEMA ) @@ -171,7 +169,9 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_CODE, - default=self.config_entry.options.get(CONF_CODE), + description={ + "suggested_value": self.config_entry.options.get(CONF_CODE) + }, ): str } ), From f858d6f9ec97a4871594f6388e15fccf929c7875 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 12 Dec 2020 19:47:46 +0100 Subject: [PATCH 422/430] Fix upnp first discovered device is used (#44151) Co-authored-by: Martin Hjelmare --- homeassistant/components/upnp/__init__.py | 14 ++++++++++++-- homeassistant/components/upnp/config_flow.py | 1 + homeassistant/components/upnp/device.py | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 773a872f33f..c9f96a0e9d7 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -43,6 +43,8 @@ async def async_discover_and_construct( ) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name + _LOGGER.debug("Constructing device: %s::%s", udn, st) + discovery_infos = await Device.async_discover(hass) _LOGGER.debug("Discovered devices: %s", discovery_infos) if not discovery_infos: @@ -53,7 +55,7 @@ async def async_discover_and_construct( # Get the discovery info with specified UDN/ST. filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn] if st: - filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st] + filtered = [di for di in filtered if di[DISCOVERY_ST] == st] if not filtered: _LOGGER.warning( 'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting', @@ -74,6 +76,7 @@ async def async_discover_and_construct( ) _LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name) + _LOGGER.debug("Constructing from discovery_info: %s", discovery_info) location = discovery_info[DISCOVERY_LOCATION] return await Device.async_create_device(hass, location) @@ -104,7 +107,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data) + _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) # Discover and construct. udn = config_entry.data.get(CONFIG_ENTRY_UDN) @@ -123,6 +126,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Ensure entry has a unique_id. if not config_entry.unique_id: + _LOGGER.debug( + "Setting unique_id: %s, for config_entry: %s", + device.unique_id, + config_entry, + ) hass.config_entries.async_update_entry( entry=config_entry, unique_id=device.unique_id, @@ -152,6 +160,8 @@ async def async_unload_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: """Unload a UPnP/IGD device from a config entry.""" + _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) + udn = config_entry.data.get(CONFIG_ENTRY_UDN) if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: del hass.data[DOMAIN][DOMAIN_DEVICES][udn] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 72efc4ffd55..7b20c7709a0 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -154,6 +154,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() # Store discovery. + _LOGGER.debug("New discovery, continuing") name = discovery_info.get("friendlyName", "") discovery = { DISCOVERY_UDN: udn, diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5f29043a1fe..6bc497170ca 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -109,7 +109,7 @@ class Device: def __str__(self) -> str: """Get string representation.""" - return f"IGD Device: {self.name}/{self.udn}" + return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" async def async_get_traffic_data(self) -> Mapping[str, any]: """ From 7c2a2b08c703f82ca44de60802469d75bfc9a3e7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Dec 2020 21:34:16 +0100 Subject: [PATCH 423/430] Updated frontend to 20201212.0 (#44154) --- 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 cb52afd4b65..caf309e6718 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/integrations/frontend", - "requirements": ["home-assistant-frontend==20201210.0"], + "requirements": ["home-assistant-frontend==20201212.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a08b3904b72..eeb104ff84c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2d7b7788db5..9a48f2623b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd704b759b5..3dff4609143 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c02be99ab767fdf7d304f0dff5b91283ac9dcd2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Dec 2020 20:35:58 +0000 Subject: [PATCH 424/430] Bumped version to 1.0.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b1a196fe232..4335c93f86a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 17cd00ba836e5058b2a77bbfb9c18e04a9e82eca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Dec 2020 22:24:16 +0100 Subject: [PATCH 425/430] Remove invalidation_version from deprecated (#44156) * Remove invalidation_version from deprecated. We don't follow up and just hurts releases * Revert change to ZHA --- .../components/arcam_fmj/__init__.py | 2 +- homeassistant/components/automation/config.py | 2 +- homeassistant/components/canary/camera.py | 2 +- .../components/cloudflare/__init__.py | 8 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/directv/__init__.py | 2 +- .../components/flunearyou/__init__.py | 2 +- homeassistant/components/hyperion/light.py | 6 +- homeassistant/components/local_ip/__init__.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/notion/__init__.py | 2 +- .../components/rainmachine/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/sentry/__init__.py | 2 +- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/withings/__init__.py | 2 +- homeassistant/components/zha/core/device.py | 2 + homeassistant/helpers/config_validation.py | 39 +--- tests/helpers/test_config_validation.py | 174 ------------------ tests/test_config.py | 2 +- 20 files changed, 25 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 0875e094352..0175dfd6586 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -23,7 +23,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def _await_cancel(task): diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 8a13334bc6d..9c26f3552aa 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -32,7 +32,7 @@ from .helpers import async_get_blueprints _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), + cv.deprecated(CONF_HIDE_ENTITY), script.make_script_schema( { # str on purpose diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index c3fd2a6ff00..fd2f08c1488 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -30,7 +30,7 @@ from .coordinator import CanaryDataUpdateCoordinator MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"), + cv.deprecated(CONF_FFMPEG_ARGUMENTS), PLATFORM_SCHEMA.extend( { vol.Optional( diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 3ebb919393a..446890887c1 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -33,10 +33,10 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_EMAIL, invalidation_version="0.119"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.119"), - cv.deprecated(CONF_ZONE, invalidation_version="0.119"), - cv.deprecated(CONF_RECORDS, invalidation_version="0.119"), + cv.deprecated(CONF_EMAIL), + cv.deprecated(CONF_API_KEY), + cv.deprecated(CONF_ZONE), + cv.deprecated(CONF_RECORDS), vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 7b9c1ded673..b4950b8b05b 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -30,7 +30,7 @@ COMPONENT_TYPES = ["climate", "sensor", "switch"] CONFIG_SCHEMA = vol.Schema( vol.All( - cv.deprecated(DOMAIN, invalidation_version="0.113.0"), + cv.deprecated(DOMAIN), { DOMAIN: vol.Schema( { diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 59682178d40..22a97b9e82e 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -22,7 +22,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 7399dd3847d..46442f112b6 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -20,7 +20,7 @@ from .const import ( DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["sensor"] diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index b8e9040f7ce..5aa087c0515 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -80,13 +80,13 @@ SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT # Usage of YAML for configuration of the Hyperion component is deprecated. PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"), + cv.deprecated(CONF_HDMI_PRIORITY), cv.deprecated(CONF_HOST), cv.deprecated(CONF_PORT), - cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"), + cv.deprecated(CONF_DEFAULT_COLOR), cv.deprecated(CONF_NAME), cv.deprecated(CONF_PRIORITY), - cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"), + cv.deprecated(CONF_EFFECT_LIST), PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index f787c028762..637520aa30c 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -11,7 +11,7 @@ from .const import DOMAIN, PLATFORM CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_NAME, invalidation_version="0.110"), + cv.deprecated(CONF_NAME), vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string}), ) }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 73caf023ef6..cced3670cca 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -191,7 +191,7 @@ def embedded_broker_deprecated(value): CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_TLS_VERSION, invalidation_version="0.115"), + cv.deprecated(CONF_TLS_VERSION), vol.Schema( { vol.Optional(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 561f3edf896..88da19f5ab2 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -30,7 +30,7 @@ ATTR_SYSTEM_NAME = "system_name" DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a520772ff77..41c56e38db6 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -74,7 +74,7 @@ SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 739e345a637..af2e0ee946f 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index eecac0281e6..6be02b9ba5e 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -33,7 +33,7 @@ from .const import ( ENTITY_COMPONENTS, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.117") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2430aad43cf..89f5c40b1ff 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -138,7 +138,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( } ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) @callback diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 94795a10c83..c6f420d172a 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -40,7 +40,7 @@ DOMAIN = const.DOMAIN CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(const.CONF_PROFILES, invalidation_version="0.114"), + cv.deprecated(const.CONF_PROFILES), vol.Schema( { vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 81b522308ff..cd3b1bd93ce 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -254,8 +254,10 @@ class ZHADevice(LogMixin): "device_event_type": "device_offline" } } + if hasattr(self._zigpy_device, "device_automation_triggers"): triggers.update(self._zigpy_device.device_automation_triggers) + return triggers @property diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index dea8deec715..0513c5c6e7e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -28,7 +28,6 @@ from typing import ( from urllib.parse import urlparse from uuid import UUID -from pkg_resources import parse_version import voluptuous as vol import voluptuous_serialize @@ -80,7 +79,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, - __version__, ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError @@ -712,7 +710,6 @@ class multi_select: def deprecated( key: str, replacement_key: Optional[str] = None, - invalidation_version: Optional[str] = None, default: Optional[Any] = None, ) -> Callable[[Dict], Dict]: """ @@ -725,8 +722,6 @@ def deprecated( - No warning if only replacement_key provided - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected """ module = inspect.getmodule(inspect.stack()[1][0]) if module is not None: @@ -737,56 +732,24 @@ def deprecated( # https://github.com/home-assistant/core/issues/24982 module_name = __name__ - if replacement_key and invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please replace it with '{replacement_key}'." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) - elif replacement_key: + if replacement_key: warning = ( "The '{key}' option is deprecated," " please replace it with '{replacement_key}'" ) - elif invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please remove it from your configuration." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) else: warning = ( "The '{key}' option is deprecated," " please remove it from your configuration" ) - def check_for_invalid_version() -> None: - """Raise error if current version has reached invalidation.""" - if not invalidation_version: - return - - if parse_version(__version__) >= parse_version(invalidation_version): - raise vol.Invalid( - warning.format( - key=key, - replacement_key=replacement_key, - invalidation_status="became", - invalidation_version=invalidation_version, - ) - ) - def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: - check_for_invalid_version() KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning, key=key, replacement_key=replacement_key, - invalidation_status="will become", - invalidation_version=invalidation_version, ) value = config[key] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 480b2280afe..5d907408b61 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -698,116 +698,6 @@ def test_deprecated_with_replacement_key(caplog, schema): assert test_data == output -def test_deprecated_with_invalidation_version(caplog, schema, version): - """ - Test deprecation behaves correctly with only an invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema without changing any values - - No warning or difference in output if key is not provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated("mars", invalidation_version="9999.99.9"), schema - ) - - message = ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. " - "This option will become invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert message in caplog.text - assert test_data == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"venus": False} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", invalidation_version="0.1.0"), schema - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. This option became " - "invalid in version 0.1.0" - ) - - -def test_deprecated_with_replacement_key_and_invalidation_version( - caplog, schema, version -): - """ - Test deprecation behaves with a replacement key & invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning or difference in output if neither key nor - replacement_key are provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", replacement_key="jupiter", invalidation_version="9999.99.9" - ), - schema, - ) - - warning = ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert warning in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_with_default(caplog, schema): """ Test deprecation behaves correctly with a default value. @@ -894,69 +784,6 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): assert {"jupiter": True} == output -def test_deprecated_with_replacement_key_invalidation_version_default( - caplog, schema, version -): - """ - Test deprecation with a replacement key, invalidation_version & default. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning if neither key nor replacement_key are provided - - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", - replacement_key="jupiter", - invalidation_version="9999.99.9", - default=False, - ), - schema, - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert {"venus": True, "jupiter": False} == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_cant_find_module(): """Test if the current module cannot be inspected.""" with patch("inspect.getmodule", return_value=None): @@ -964,7 +791,6 @@ def test_deprecated_cant_find_module(): cv.deprecated( "mars", replacement_key="jupiter", - invalidation_version="1.0.0", default=False, ) diff --git a/tests/test_config.py b/tests/test_config.py index bfda156f2b7..931b672d01b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1116,7 +1116,7 @@ async def test_component_config_exceptions(hass, caplog): ("non_existing", vol.Schema({"zone": int}), None), ("zone", vol.Schema({}), None), ("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"), - ("openuv", cv.deprecated("openuv", invalidation_version="0.115"), None), + ("openuv", cv.deprecated("openuv"), None), ], ) def test_identify_config_schema(domain, schema, expected): From 9c45874206be5a7af35eb7ac248450c8c81b9bf0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Dec 2020 05:36:17 -0600 Subject: [PATCH 426/430] Bump zeroconf to 0.28.7 to fix thread safety (#44160) Service registration was not thread safe --- 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 1848c890573..753ac2a2441 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.6"], + "requirements": ["zeroconf==0.28.7"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eeb104ff84c..7aa59bd5836 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 voluptuous==0.12.0 yarl==1.4.2 -zeroconf==0.28.6 +zeroconf==0.28.7 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 9a48f2623b6..0a0af294a85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,7 +2342,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.6 +zeroconf==0.28.7 # homeassistant.components.zha zha-quirks==0.0.48 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dff4609143..4a416873fba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1141,7 +1141,7 @@ yeelight==0.5.4 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.28.6 +zeroconf==0.28.7 # homeassistant.components.zha zha-quirks==0.0.48 From 3981592ccd9c5fc6db708ac8f60dd94ec83fe531 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 13 Dec 2020 08:43:33 -0500 Subject: [PATCH 427/430] Bump the ZHA quirks lib to 0.0.49 (#44173) --- 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 f1821c9e480..bcaa4038de1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.21.0", "pyserial==3.4", "pyserial-asyncio==0.4", - "zha-quirks==0.0.48", + "zha-quirks==0.0.49", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", "zigpy==0.28.2", diff --git a/requirements_all.txt b/requirements_all.txt index 0a0af294a85..858b747b2ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2345,7 +2345,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.48 +zha-quirks==0.0.49 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a416873fba..9dc1bd8d860 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.48 +zha-quirks==0.0.49 # homeassistant.components.zha zigpy-cc==0.5.2 From 1ac5ecbe694fe1c4dacf8c6d11cd3de526754423 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Dec 2020 15:04:00 +0100 Subject: [PATCH 428/430] Bumped version to 1.0.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4335c93f86a..a5ffbff4cd8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 623826261bd74918815692f326bbd79d6f24d97e Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Thu, 10 Dec 2020 21:50:51 +0100 Subject: [PATCH 429/430] Remove deprecated CONF_ALLOW_UNLOCK, CONF_API_KEY from Google Assistant (#44087) * Remove deprecated CONF_ALLOW_UNLOCK, CONF_API_KEY * Use vol.Remove() to prevent setup fail * Keep constants --- .../components/google_assistant/__init__.py | 14 +++++----- .../components/google_assistant/const.py | 2 -- .../components/google_assistant/http.py | 26 +------------------ 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 0afd66c10aa..8f4ee3b51c4 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -11,8 +11,6 @@ from homeassistant.helpers import config_validation as cv from .const import ( CONF_ALIASES, - CONF_ALLOW_UNLOCK, - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -36,6 +34,9 @@ from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, is _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_UNLOCK = "allow_unlock" +CONF_API_KEY = "api_key" + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, @@ -61,8 +62,6 @@ 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, @@ -72,13 +71,14 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( vol.Optional( CONF_EXPOSED_DOMAINS, default=DEFAULT_EXPOSED_DOMAINS ): cv.ensure_list, - vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, - 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, + # deprecated configuration options + vol.Remove(CONF_ALLOW_UNLOCK): cv.boolean, + vol.Remove(CONF_API_KEY): cv.string, }, extra=vol.PREVENT_EXTRA, ), @@ -113,7 +113,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): 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: + if CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 47ceabb20e8..d6badf2e7ba 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -30,9 +30,7 @@ CONF_EXPOSE_BY_DEFAULT = "expose_by_default" CONF_EXPOSED_DOMAINS = "exposed_domains" CONF_PROJECT_ID = "project_id" CONF_ALIASES = "aliases" -CONF_API_KEY = "api_key" 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" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 4bf0df8b933..5cf1cb14379 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -19,7 +19,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from .const import ( - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -135,11 +134,7 @@ class GoogleConfig(AbstractConfig): 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: + if CONF_SERVICE_ACCOUNT in self._config: await self.async_call_homegraph_api( REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} ) @@ -164,25 +159,6 @@ class GoogleConfig(AbstractConfig): 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 HTTP_INTERNAL_SERVER_ERROR - async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authentication.""" session = async_get_clientsession(self.hass) From 277b916fcdfd7eab4c0507d7136449d287250715 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Dec 2020 22:13:58 +0100 Subject: [PATCH 430/430] 2020.12.0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a5ffbff4cd8..1e7d243b9ad 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 1 -MINOR_VERSION = 0 +MAJOR_VERSION = 2020 +MINOR_VERSION = 12 PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"