From 8c178adf4f6de9cf3fd8d75a3a12bd191b4b2ecc Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 30 Jan 2020 00:31:41 +0000 Subject: [PATCH 001/378] [ci skip] Translation update --- .../brother/.translations/zh-Hant.json | 8 +++++++ .../garmin_connect/.translations/lb.json | 24 +++++++++++++++++++ .../garmin_connect/.translations/zh-Hant.json | 24 +++++++++++++++++++ .../components/linky/.translations/lb.json | 1 + .../linky/.translations/zh-Hant.json | 1 + .../components/spotify/.translations/lb.json | 18 ++++++++++++++ .../spotify/.translations/zh-Hant.json | 18 ++++++++++++++ 7 files changed, 94 insertions(+) create mode 100644 homeassistant/components/garmin_connect/.translations/lb.json create mode 100644 homeassistant/components/garmin_connect/.translations/zh-Hant.json create mode 100644 homeassistant/components/spotify/.translations/lb.json create mode 100644 homeassistant/components/spotify/.translations/zh-Hant.json diff --git a/homeassistant/components/brother/.translations/zh-Hant.json b/homeassistant/components/brother/.translations/zh-Hant.json index cff89ea38ca..0ef813dffea 100644 --- a/homeassistant/components/brother/.translations/zh-Hant.json +++ b/homeassistant/components/brother/.translations/zh-Hant.json @@ -9,6 +9,7 @@ "snmp_error": "SNMP \u4f3a\u670d\u5668\u70ba\u95dc\u9589\u72c0\u614b\u6216\u5370\u8868\u6a5f\u4e0d\u652f\u63f4\u3002", "wrong_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740" }, + "flow_title": "Brother \u5370\u8868\u6a5f\uff1a{model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "\u8a2d\u5b9a Brother \u5370\u8868\u6a5f\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/brother", "title": "Brother \u5370\u8868\u6a5f" + }, + "zeroconf_confirm": { + "data": { + "type": "\u5370\u8868\u6a5f\u985e\u578b" + }, + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f {serial_number} \u4e4bBrother \u5370\u8868\u6a5f {model} \u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u767c\u73fe Brother \u5370\u8868\u6a5f" } }, "title": "Brother \u5370\u8868\u6a5f" diff --git a/homeassistant/components/garmin_connect/.translations/lb.json b/homeassistant/components/garmin_connect/.translations/lb.json new file mode 100644 index 00000000000..8289be66d59 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun.", + "too_many_requests": "Ze vill Ufroen, prob\u00e9iert sp\u00e9ider nach emol.", + "unknown": "Onerwaarte Feeler." + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/zh-Hant.json b/homeassistant/components/garmin_connect/.translations/zh-Hant.json new file mode 100644 index 00000000000..8ddb5e61295 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_auth": "\u9a57\u8b49\u7121\u6548\u3002", + "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165\u6191\u8b49\u3002", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json index cd3c7152c89..8279b8a7d6f 100644 --- a/homeassistant/components/linky/.translations/lb.json +++ b/homeassistant/components/linky/.translations/lb.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert", "username_exists": "Kont ass scho konfigur\u00e9iert" }, "error": { diff --git a/homeassistant/components/linky/.translations/zh-Hant.json b/homeassistant/components/linky/.translations/zh-Hant.json index bcfac6643c8..51622e2f6f7 100644 --- a/homeassistant/components/linky/.translations/zh-Hant.json +++ b/homeassistant/components/linky/.translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { diff --git a/homeassistant/components/spotify/.translations/lb.json b/homeassistant/components/spotify/.translations/lb.json new file mode 100644 index 00000000000..b7d555cbce1 --- /dev/null +++ b/homeassistant/components/spotify/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Spotify Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "Spotifiy Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Spotify authentifiz\u00e9iert." + }, + "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + } + }, + "title": "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 new file mode 100644 index 00000000000..c4ba3d46343 --- /dev/null +++ b/homeassistant/components/spotify/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Spotify \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "Spotify \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Spotify\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + }, + "title": "Spotify" + } +} \ No newline at end of file From cf0e467150752a1ed518ab0f4ac0372f66271e5d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 29 Jan 2020 17:15:47 -0800 Subject: [PATCH 002/378] Change scan_interval defaults for Tesla (#31194) --- homeassistant/components/tesla/__init__.py | 21 +++++++++++++------ homeassistant/components/tesla/config_flow.py | 10 +++++---- homeassistant/components/tesla/const.py | 2 ++ tests/components/tesla/test_config_flow.py | 8 +++---- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 3c2a22793db..df0664b8f4c 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -27,7 +27,14 @@ from .config_flow import ( configured_instances, validate_input, ) -from .const import DATA_LISTENER, DOMAIN, ICONS, TESLA_COMPONENTS +from .const import ( + DATA_LISTENER, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ICONS, + MIN_SCAN_INTERVAL, + TESLA_COMPONENTS, +) _LOGGER = logging.getLogger(__name__) @@ -37,9 +44,9 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=300): vol.All( - cv.positive_int, vol.Clamp(min=300) - ), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)), } ) }, @@ -64,7 +71,7 @@ async def async_setup(hass, base_config): def _update_entry(email, data=None, options=None): data = data or {} - options = options or {CONF_SCAN_INTERVAL: 300} + options = options or {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} for entry in hass.config_entries.async_entries(DOMAIN): if email != entry.title: continue @@ -120,7 +127,9 @@ async def async_setup_entry(hass, config_entry): websession, refresh_token=config[CONF_TOKEN], access_token=config[CONF_ACCESS_TOKEN], - update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300), + update_interval=config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), ) (refresh_token, access_token) = await controller.connect() except TeslaException as ex: diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 2d2bc0158d2..c719807da9f 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -100,8 +100,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_SCAN_INTERVAL, - default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300), - ): vol.All(cv.positive_int, vol.Clamp(min=300)) + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)) } ) return self.async_show_form(step_id="init", data_schema=data_schema) @@ -120,7 +122,7 @@ async def validate_input(hass: core.HomeAssistant, data): websession, email=data[CONF_USERNAME], password=data[CONF_PASSWORD], - update_interval=300, + update_interval=DEFAULT_SCAN_INTERVAL, ) (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( test_login=True diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index be460a430ac..54cb7a2e071 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -1,6 +1,8 @@ """Const file for Tesla cars.""" DOMAIN = "tesla" DATA_LISTENER = "listener" +DEFAULT_SCAN_INTERVAL = 660 +MIN_SCAN_INTERVAL = 60 TESLA_COMPONENTS = [ "sensor", "lock", diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 7b7e822ce58..477583f23fb 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from teslajsonpy import TeslaException from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.tesla.const import DOMAIN +from homeassistant.components.tesla.const import DOMAIN, MIN_SCAN_INTERVAL from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_PASSWORD, @@ -40,8 +40,8 @@ async def test_form(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "test@email.com" assert result2["data"] == { - "token": "test-refresh-token", - "access_token": "test-access-token", + CONF_TOKEN: "test-refresh-token", + CONF_ACCESS_TOKEN: "test-access-token", } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -157,4 +157,4 @@ async def test_option_flow_input_floor(hass): result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1} ) assert result["type"] == "create_entry" - assert result["data"] == {CONF_SCAN_INTERVAL: 300} + assert result["data"] == {CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL} From da14e2927ffce3737bed6169a30616a1607f03da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Jan 2020 21:59:24 -0800 Subject: [PATCH 003/378] Removes I/O from linky tests (#31299) --- tests/components/linky/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/components/linky/conftest.py diff --git a/tests/components/linky/conftest.py b/tests/components/linky/conftest.py new file mode 100644 index 00000000000..f77f01a4ae7 --- /dev/null +++ b/tests/components/linky/conftest.py @@ -0,0 +1,11 @@ +"""Linky generic test utils.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def patch_fakeuseragent(): + """Stub out fake useragent dep that makes requests.""" + with patch("pylinky.client.UserAgent", return_value="Test Browser"): + yield From cad451d2b7f2da1bb9e8b920fa91f1a3b068313e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 01:06:17 -0800 Subject: [PATCH 004/378] Add zone to defaul config (#31303) --- homeassistant/components/default_config/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index c0a27b667c5..e19b1262b74 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -18,7 +18,8 @@ "sun", "system_health", "updater", - "zeroconf" + "zeroconf", + "zone" ], "codeowners": [] } From ea666248cef20ec7382bdaa3dc7722e31dbbf754 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 01:09:06 -0800 Subject: [PATCH 005/378] Add zones services.yaml (#31298) --- homeassistant/components/zone/services.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 homeassistant/components/zone/services.yaml diff --git a/homeassistant/components/zone/services.yaml b/homeassistant/components/zone/services.yaml new file mode 100644 index 00000000000..550eee24fab --- /dev/null +++ b/homeassistant/components/zone/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload the YAML-based zone configuration. From 7ff30fe29d2cc4e3af74cc7ffc01677ddc6f0ea6 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 30 Jan 2020 04:47:44 -0500 Subject: [PATCH 006/378] Reorganize insteon code (#31183) * Reorganize code * Code review --- homeassistant/components/insteon/__init__.py | 649 +----------------- .../components/insteon/binary_sensor.py | 2 +- homeassistant/components/insteon/const.py | 106 +++ homeassistant/components/insteon/cover.py | 2 +- homeassistant/components/insteon/fan.py | 2 +- .../components/insteon/insteon_entity.py | 123 ++++ homeassistant/components/insteon/ipdb.py | 82 +++ homeassistant/components/insteon/light.py | 2 +- homeassistant/components/insteon/schemas.py | 153 +++++ homeassistant/components/insteon/sensor.py | 2 +- homeassistant/components/insteon/switch.py | 2 +- homeassistant/components/insteon/utils.py | 239 +++++++ 12 files changed, 740 insertions(+), 624 deletions(-) create mode 100644 homeassistant/components/insteon/const.py create mode 100644 homeassistant/components/insteon/insteon_entity.py create mode 100644 homeassistant/components/insteon/ipdb.py create mode 100644 homeassistant/components/insteon/schemas.py create mode 100644 homeassistant/components/insteon/utils.py diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index df6fa626a4f..ce17cc6c77d 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,278 +1,43 @@ """Support for INSTEON Modems (PLM and Hub).""" -import collections import logging -from typing import Dict import insteonplm -from insteonplm.devices import ALDBStatus -from insteonplm.states.cover import Cover -from insteonplm.states.dimmable import ( - DimmableKeypadA, - DimmableRemote, - DimmableSwitch, - DimmableSwitch_Fan, -) -from insteonplm.states.onOff import ( - OnOffKeypad, - OnOffKeypadA, - OnOffSwitch, - OnOffSwitch_OutletBottom, - OnOffSwitch_OutletTop, - OpenClosedRelay, -) -from insteonplm.states.sensor import ( - IoLincSensor, - LeakSensorDryWet, - OnOffSensor, - SmokeCO2Sensor, - VariableSensor, -) -from insteonplm.states.x10 import ( - X10AllLightsOffSensor, - X10AllLightsOnSensor, - X10AllUnitsOffSensor, - X10DimmableSwitch, - X10OnOffSensor, - X10OnOffSwitch, -) -import voluptuous as vol from homeassistant.const import ( - CONF_ADDRESS, - CONF_ENTITY_ID, CONF_HOST, CONF_PLATFORM, CONF_PORT, - ENTITY_MATCH_ALL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity + +from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_FIRMWARE, + CONF_HOUSECODE, + CONF_HUB_PASSWORD, + CONF_HUB_USERNAME, + CONF_HUB_VERSION, + CONF_IP_PORT, + CONF_OVERRIDE, + CONF_PRODUCT_KEY, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + CONF_X10_ALL_LIGHTS_OFF, + CONF_X10_ALL_LIGHTS_ON, + CONF_X10_ALL_UNITS_OFF, + DOMAIN, + INSTEON_ENTITIES, +) +from .schemas import CONFIG_SCHEMA # noqa F440 +from .utils import async_register_services, register_new_device_callback _LOGGER = logging.getLogger(__name__) -DOMAIN = "insteon" -INSTEON_ENTITIES = "entities" - -CONF_IP_PORT = "ip_port" -CONF_HUB_USERNAME = "username" -CONF_HUB_PASSWORD = "password" -CONF_HUB_VERSION = "hub_version" -CONF_OVERRIDE = "device_override" -CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host" -CONF_CAT = "cat" -CONF_SUBCAT = "subcat" -CONF_FIRMWARE = "firmware" -CONF_PRODUCT_KEY = "product_key" -CONF_X10 = "x10_devices" -CONF_HOUSECODE = "housecode" -CONF_UNITCODE = "unitcode" -CONF_DIM_STEPS = "dim_steps" -CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" -CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" -CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" - -SRV_ADD_ALL_LINK = "add_all_link" -SRV_DEL_ALL_LINK = "delete_all_link" -SRV_LOAD_ALDB = "load_all_link_database" -SRV_PRINT_ALDB = "print_all_link_database" -SRV_PRINT_IM_ALDB = "print_im_all_link_database" -SRV_X10_ALL_UNITS_OFF = "x10_all_units_off" -SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" -SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on" -SRV_ALL_LINK_GROUP = "group" -SRV_ALL_LINK_MODE = "mode" -SRV_LOAD_DB_RELOAD = "reload" -SRV_CONTROLLER = "controller" -SRV_RESPONDER = "responder" -SRV_HOUSECODE = "housecode" -SRV_SCENE_ON = "scene_on" -SRV_SCENE_OFF = "scene_off" - -SIGNAL_LOAD_ALDB = "load_aldb" -SIGNAL_PRINT_ALDB = "print_aldb" - -HOUSECODES = [ - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "l", - "m", - "n", - "o", - "p", -] - -BUTTON_PRESSED_STATE_NAME = "onLevelButton" -EVENT_BUTTON_ON = "insteon.button_on" -EVENT_BUTTON_OFF = "insteon.button_off" -EVENT_CONF_BUTTON = "button" - - -def set_default_port(schema: Dict) -> Dict: - """Set the default port based on the Hub version.""" - # If the ip_port is found do nothing - # If it is not found the set the default - ip_port = schema.get(CONF_IP_PORT) - if not ip_port: - hub_version = schema.get(CONF_HUB_VERSION) - # Found hub_version but not ip_port - if hub_version == 1: - schema[CONF_IP_PORT] = 9761 - else: - schema[CONF_IP_PORT] = 25105 - return schema - - -CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( - cv.deprecated(CONF_PLATFORM), - vol.Schema( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_CAT): cv.byte, - vol.Optional(CONF_SUBCAT): cv.byte, - vol.Optional(CONF_FIRMWARE): cv.byte, - vol.Optional(CONF_PRODUCT_KEY): cv.byte, - vol.Optional(CONF_PLATFORM): cv.string, - } - ), -) - - -CONF_X10_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOUSECODE): cv.string, - vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), - vol.Required(CONF_PLATFORM): cv.string, - vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255), - } - ) -) - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - vol.Schema( - { - vol.Exclusive( - CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG - ): cv.string, - vol.Exclusive( - CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG - ): cv.string, - vol.Optional(CONF_IP_PORT): cv.port, - vol.Optional(CONF_HUB_USERNAME): cv.string, - vol.Optional(CONF_HUB_PASSWORD): cv.string, - vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), - vol.Optional(CONF_OVERRIDE): vol.All( - cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA] - ), - vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), - vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), - vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), - vol.Optional(CONF_X10): vol.All( - cv.ensure_list_csv, [CONF_X10_SCHEMA] - ), - }, - extra=vol.ALLOW_EXTRA, - required=True, - ), - cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - set_default_port, - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -ADD_ALL_LINK_SCHEMA = vol.Schema( - { - vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), - vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), - } -) - - -DEL_ALL_LINK_SCHEMA = vol.Schema( - {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} -) - - -LOAD_ALDB_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), - vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, - } -) - - -PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) - - -X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)}) - - -TRIGGER_SCENE_SCHEMA = vol.Schema( - {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} -) - - -STATE_NAME_LABEL_MAP = { - "keypadButtonA": "Button A", - "keypadButtonB": "Button B", - "keypadButtonC": "Button C", - "keypadButtonD": "Button D", - "keypadButtonE": "Button E", - "keypadButtonF": "Button F", - "keypadButtonG": "Button G", - "keypadButtonH": "Button H", - "keypadButtonMain": "Main", - "onOffButtonA": "Button A", - "onOffButtonB": "Button B", - "onOffButtonC": "Button C", - "onOffButtonD": "Button D", - "onOffButtonE": "Button E", - "onOffButtonF": "Button F", - "onOffButtonG": "Button G", - "onOffButtonH": "Button H", - "onOffButtonMain": "Main", - "fanOnLevel": "Fan", - "lightOnLevel": "Light", - "coolSetPoint": "Cool Set", - "heatSetPoint": "HeatSet", - "statusReport": "Status", - "generalSensor": "Sensor", - "motionSensor": "Motion", - "lightSensor": "Light", - "batterySensor": "Battery", - "dryLeakSensor": "Dry", - "wetLeakSensor": "Wet", - "heartbeatLeakSensor": "Heartbeat", - "openClosedRelay": "Relay", - "openClosedSensor": "Sensor", - "lightOnOff": "Light", - "outletTopOnOff": "Top", - "outletBottomOnOff": "Bottom", - "coverOpenLevel": "Cover", -} - async def async_setup(hass, config): """Set up the connection to the modem.""" - ipdb = IPDB() insteon_modem = None conf = config[DOMAIN] @@ -288,163 +53,6 @@ async def async_setup(hass, config): x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) - @callback - def async_new_insteon_device(device): - """Detect device from transport to be delegated to platform.""" - for state_key in device.states: - platform_info = ipdb[device.states[state_key]] - if platform_info and platform_info.platform: - platform = platform_info.platform - - if platform == "on_off_events": - device.states[state_key].register_updates(_fire_button_on_off_event) - - else: - _LOGGER.info( - "New INSTEON device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform, - ) - - hass.async_create_task( - discovery.async_load_platform( - hass, - platform, - DOMAIN, - discovered={ - "address": device.address.id, - "state_key": state_key, - }, - hass_config=config, - ) - ) - - def add_all_link(service): - """Add an INSTEON All-Link between two devices.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - mode = service.data.get(SRV_ALL_LINK_MODE) - link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 - insteon_modem.start_all_linking(link_mode, group) - - def del_all_link(service): - """Delete an INSTEON All-Link between two devices.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.start_all_linking(255, group) - - def load_aldb(service): - """Load the device All-Link database.""" - entity_id = service.data[CONF_ENTITY_ID] - reload = service.data[SRV_LOAD_DB_RELOAD] - if entity_id.lower() == ENTITY_MATCH_ALL: - for entity_id in hass.data[DOMAIN].get(INSTEON_ENTITIES): - _send_load_aldb_signal(entity_id, reload) - else: - _send_load_aldb_signal(entity_id, reload) - - def _send_load_aldb_signal(entity_id, reload): - """Send the load All-Link database signal to INSTEON entity.""" - signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" - dispatcher_send(hass, signal, reload) - - def print_aldb(service): - """Print the All-Link Database for a device.""" - # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. - entity_id = service.data[CONF_ENTITY_ID] - signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" - dispatcher_send(hass, signal) - - def print_im_aldb(service): - """Print the All-Link Database for a device.""" - # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. - print_aldb_to_log(insteon_modem.aldb) - - def x10_all_units_off(service): - """Send the X10 All Units Off command.""" - housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_units_off(housecode) - - def x10_all_lights_off(service): - """Send the X10 All Lights Off command.""" - housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_lights_off(housecode) - - def x10_all_lights_on(service): - """Send the X10 All Lights On command.""" - housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_lights_on(housecode) - - def scene_on(service): - """Trigger an INSTEON scene ON.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_on(group) - - def scene_off(service): - """Trigger an INSTEON scene ON.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_off(group) - - def _register_services(): - hass.services.register( - DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA - ) - hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) - hass.services.register( - DOMAIN, - SRV_X10_ALL_UNITS_OFF, - x10_all_units_off, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SRV_X10_ALL_LIGHTS_OFF, - x10_all_lights_off, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SRV_X10_ALL_LIGHTS_ON, - x10_all_lights_on, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA - ) - _LOGGER.debug("Insteon Services registered") - - def _fire_button_on_off_event(address, group, val): - # Firing an event when a button is pressed. - device = insteon_modem.devices[address.hex] - state_name = device.states[group].name - button = ( - "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower() - ) - schema = {CONF_ADDRESS: address.hex} - if button != "": - schema[EVENT_CONF_BUTTON] = button - if val: - event = EVENT_BUTTON_ON - else: - event = EVENT_BUTTON_OFF - _LOGGER.debug( - "Firing event %s with address %s and button %s", event, address.hex, button - ) - hass.bus.fire(event, schema) - if host: _LOGGER.info("Connecting to Insteon Hub on %s", host) conn = await insteonplm.Connection.create( @@ -464,6 +72,14 @@ async def async_setup(hass, config): insteon_modem = conn.protocol + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["modem"] = insteon_modem + hass.data[DOMAIN][INSTEON_ENTITIES] = set() + + register_new_device_callback(hass, config, insteon_modem) + async_register_services(hass, config, insteon_modem) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) + for device_override in overrides: # # Override the device default capabilities for a specific address @@ -477,14 +93,6 @@ async def async_setup(hass, config): address, CONF_PRODUCT_KEY, device_override[prop] ) - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["modem"] = insteon_modem - hass.data[DOMAIN][INSTEON_ENTITIES] = {} - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) - - insteon_modem.devices.add_device_callback(async_new_insteon_device) - if x10_all_units_off_housecode: device = insteon_modem.add_x10_device( x10_all_units_off_housecode, 20, "allunitsoff" @@ -513,199 +121,4 @@ async def async_setup(hass, config): if device and hasattr(device.states[0x01], "steps"): device.states[0x01].steps = steps - hass.async_add_job(_register_services) - return True - - -State = collections.namedtuple("Product", "stateType platform") - - -class IPDB: - """Embodies the INSTEON Product Database static data and access methods.""" - - def __init__(self): - """Create the INSTEON Product Database (IPDB).""" - self.states = [ - State(Cover, "cover"), - State(OnOffSwitch_OutletTop, "switch"), - State(OnOffSwitch_OutletBottom, "switch"), - State(OpenClosedRelay, "switch"), - State(OnOffSwitch, "switch"), - State(OnOffKeypadA, "switch"), - State(OnOffKeypad, "switch"), - State(LeakSensorDryWet, "binary_sensor"), - State(IoLincSensor, "binary_sensor"), - State(SmokeCO2Sensor, "sensor"), - State(OnOffSensor, "binary_sensor"), - State(VariableSensor, "sensor"), - State(DimmableSwitch_Fan, "fan"), - State(DimmableSwitch, "light"), - State(DimmableRemote, "on_off_events"), - State(DimmableKeypadA, "light"), - State(X10DimmableSwitch, "light"), - State(X10OnOffSwitch, "switch"), - State(X10OnOffSensor, "binary_sensor"), - State(X10AllUnitsOffSensor, "binary_sensor"), - State(X10AllLightsOnSensor, "binary_sensor"), - State(X10AllLightsOffSensor, "binary_sensor"), - ] - - def __len__(self): - """Return the number of INSTEON state types mapped to HA platforms.""" - return len(self.states) - - def __iter__(self): - """Itterate through the INSTEON state types to HA platforms.""" - for product in self.states: - yield product - - def __getitem__(self, key): - """Return a Home Assistant platform from an INSTEON state type.""" - for state in self.states: - if isinstance(key, state.stateType): - return state - return None - - -class InsteonEntity(Entity): - """INSTEON abstract base entity.""" - - def __init__(self, device, state_key): - """Initialize the INSTEON binary sensor.""" - self._insteon_device_state = device.states[state_key] - self._insteon_device = device - self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def address(self): - """Return the address of the node.""" - return self._insteon_device.address.human - - @property - def group(self): - """Return the INSTEON group that the entity responds to.""" - return self._insteon_device_state.group - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - if self._insteon_device_state.group == 0x01: - uid = self._insteon_device.id - else: - uid = "{:s}_{:d}".format( - self._insteon_device.id, self._insteon_device_state.group - ) - return uid - - @property - def name(self): - """Return the name of the node (used for Entity_ID).""" - # Set a base description - description = self._insteon_device.description - if self._insteon_device.description is None: - description = "Unknown Device" - - # Get an extension label if there is one - extension = self._get_label() - if extension: - extension = f" {extension}" - name = "{:s} {:s}{:s}".format( - description, self._insteon_device.address.human, extension - ) - return name - - @property - def device_state_attributes(self): - """Provide attributes for display on device card.""" - attributes = {"INSTEON Address": self.address, "INSTEON Group": self.group} - return attributes - - @callback - def async_entity_update(self, deviceid, group, val): - """Receive notification from transport that new data exists.""" - _LOGGER.debug( - "Received update for device %s group %d value %s", - deviceid.human, - group, - val, - ) - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Register INSTEON update events.""" - _LOGGER.debug( - "Tracking updates for device %s group %d statename %s", - self.address, - self.group, - self._insteon_device_state.name, - ) - self._insteon_device_state.register_updates(self.async_entity_update) - self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self - load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" - async_dispatcher_connect(self.hass, load_signal, self._load_aldb) - print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" - async_dispatcher_connect(self.hass, print_signal, self._print_aldb) - - def _load_aldb(self, reload=False): - """Load the device All-Link Database.""" - if reload: - self._insteon_device.aldb.clear() - self._insteon_device.read_aldb() - - def _print_aldb(self): - """Print the device ALDB to the log file.""" - print_aldb_to_log(self._insteon_device.aldb) - - @callback - def _aldb_loaded(self): - """All-Link Database loaded for the device.""" - self._print_aldb() - - def _get_label(self): - """Get the device label for grouped devices.""" - label = "" - if len(self._insteon_device.states) > 1: - if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: - label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] - else: - label = f"Group {self.group:d}" - return label - - -def print_aldb_to_log(aldb): - """Print the All-Link Database to the log file.""" - _LOGGER.info("ALDB load status is %s", aldb.status.name) - if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: - _LOGGER.warning("Device All-Link database not loaded") - _LOGGER.warning("Use service insteon.load_aldb first") - return - - _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") - _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") - for mem_addr in aldb: - rec = aldb[mem_addr] - # For now we write this to the log - # Roadmap is to create a configuration panel - in_use = "Y" if rec.control_flags.is_in_use else "N" - mode = "C" if rec.control_flags.is_controller else "R" - hwm = "Y" if rec.control_flags.is_high_water_mark else "N" - _LOGGER.info( - " {:04x} {:s} {:s} {:s} {:3d} {:s}" - " {:3d} {:3d} {:3d}".format( - rec.mem_addr, - in_use, - mode, - hwm, - rec.group, - rec.address.human, - rec.data1, - rec.data2, - rec.data3, - ) - ) diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 68ea07cdb49..395c0a3ac20 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py new file mode 100644 index 00000000000..b01409f49ff --- /dev/null +++ b/homeassistant/components/insteon/const.py @@ -0,0 +1,106 @@ +"""Constants used by insteon component.""" + +DOMAIN = "insteon" +INSTEON_ENTITIES = "entities" + +CONF_IP_PORT = "ip_port" +CONF_HUB_USERNAME = "username" +CONF_HUB_PASSWORD = "password" +CONF_HUB_VERSION = "hub_version" +CONF_OVERRIDE = "device_override" +CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host" +CONF_CAT = "cat" +CONF_SUBCAT = "subcat" +CONF_FIRMWARE = "firmware" +CONF_PRODUCT_KEY = "product_key" +CONF_X10 = "x10_devices" +CONF_HOUSECODE = "housecode" +CONF_UNITCODE = "unitcode" +CONF_DIM_STEPS = "dim_steps" +CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" +CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" +CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" + +SRV_ADD_ALL_LINK = "add_all_link" +SRV_DEL_ALL_LINK = "delete_all_link" +SRV_LOAD_ALDB = "load_all_link_database" +SRV_PRINT_ALDB = "print_all_link_database" +SRV_PRINT_IM_ALDB = "print_im_all_link_database" +SRV_X10_ALL_UNITS_OFF = "x10_all_units_off" +SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" +SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on" +SRV_ALL_LINK_GROUP = "group" +SRV_ALL_LINK_MODE = "mode" +SRV_LOAD_DB_RELOAD = "reload" +SRV_CONTROLLER = "controller" +SRV_RESPONDER = "responder" +SRV_HOUSECODE = "housecode" +SRV_SCENE_ON = "scene_on" +SRV_SCENE_OFF = "scene_off" + +SIGNAL_LOAD_ALDB = "load_aldb" +SIGNAL_PRINT_ALDB = "print_aldb" + +HOUSECODES = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", +] + +BUTTON_PRESSED_STATE_NAME = "onLevelButton" +EVENT_BUTTON_ON = "insteon.button_on" +EVENT_BUTTON_OFF = "insteon.button_off" +EVENT_CONF_BUTTON = "button" + + +STATE_NAME_LABEL_MAP = { + "keypadButtonA": "Button A", + "keypadButtonB": "Button B", + "keypadButtonC": "Button C", + "keypadButtonD": "Button D", + "keypadButtonE": "Button E", + "keypadButtonF": "Button F", + "keypadButtonG": "Button G", + "keypadButtonH": "Button H", + "keypadButtonMain": "Main", + "onOffButtonA": "Button A", + "onOffButtonB": "Button B", + "onOffButtonC": "Button C", + "onOffButtonD": "Button D", + "onOffButtonE": "Button E", + "onOffButtonF": "Button F", + "onOffButtonG": "Button G", + "onOffButtonH": "Button H", + "onOffButtonMain": "Main", + "fanOnLevel": "Fan", + "lightOnLevel": "Light", + "coolSetPoint": "Cool Set", + "heatSetPoint": "HeatSet", + "statusReport": "Status", + "generalSensor": "Sensor", + "motionSensor": "Motion", + "lightSensor": "Light", + "batterySensor": "Battery", + "dryLeakSensor": "Dry", + "wetLeakSensor": "Wet", + "heartbeatLeakSensor": "Heartbeat", + "openClosedRelay": "Relay", + "openClosedSensor": "Sensor", + "lightOnOff": "Light", + "outletTopOnOff": "Top", + "outletBottomOnOff": "Bottom", + "coverOpenLevel": "Cover", +} diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index f9399d7b13f..575799cbf67 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( CoverDevice, ) -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index d88348b1a5d..6ad7436faf5 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import STATE_OFF -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py new file mode 100644 index 00000000000..c489dd8e382 --- /dev/null +++ b/homeassistant/components/insteon/insteon_entity.py @@ -0,0 +1,123 @@ +"""Insteon base entity.""" +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import ( + DOMAIN, + INSTEON_ENTITIES, + SIGNAL_LOAD_ALDB, + SIGNAL_PRINT_ALDB, + STATE_NAME_LABEL_MAP, +) +from .utils import print_aldb_to_log + +_LOGGER = logging.getLogger(__name__) + + +class InsteonEntity(Entity): + """INSTEON abstract base entity.""" + + def __init__(self, device, state_key): + """Initialize the INSTEON binary sensor.""" + self._insteon_device_state = device.states[state_key] + self._insteon_device = device + self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def address(self): + """Return the address of the node.""" + return self._insteon_device.address.human + + @property + def group(self): + """Return the INSTEON group that the entity responds to.""" + return self._insteon_device_state.group + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + if self._insteon_device_state.group == 0x01: + uid = self._insteon_device.id + else: + uid = f"{self._insteon_device.id}_{self._insteon_device_state.group}" + return uid + + @property + def name(self): + """Return the name of the node (used for Entity_ID).""" + # Set a base description + description = self._insteon_device.description + if self._insteon_device.description is None: + description = "Unknown Device" + + # Get an extension label if there is one + extension = self._get_label() + if extension: + extension = f" {extension}" + name = f"{description} {self._insteon_device.address.human}{extension}" + return name + + @property + def device_state_attributes(self): + """Provide attributes for display on device card.""" + attributes = {"insteon_address": self.address, "insteon_group": self.group} + return attributes + + @callback + def async_entity_update(self, deviceid, group, val): + """Receive notification from transport that new data exists.""" + _LOGGER.debug( + "Received update for device %s group %d value %s", + deviceid.human, + group, + val, + ) + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Register INSTEON update events.""" + _LOGGER.debug( + "Tracking updates for device %s group %d statename %s", + self.address, + self.group, + self._insteon_device_state.name, + ) + self._insteon_device_state.register_updates(self.async_entity_update) + self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id) + load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" + async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" + async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + + def _load_aldb(self, reload=False): + """Load the device All-Link Database.""" + if reload: + self._insteon_device.aldb.clear() + self._insteon_device.read_aldb() + + def _print_aldb(self): + """Print the device ALDB to the log file.""" + print_aldb_to_log(self._insteon_device.aldb) + + @callback + def _aldb_loaded(self): + """All-Link Database loaded for the device.""" + self._print_aldb() + + def _get_label(self): + """Get the device label for grouped devices.""" + label = "" + if len(self._insteon_device.states) > 1: + if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: + label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] + else: + label = f"Group {self.group:d}" + return label diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py new file mode 100644 index 00000000000..1618518a0eb --- /dev/null +++ b/homeassistant/components/insteon/ipdb.py @@ -0,0 +1,82 @@ +"""Insteon product database.""" +import collections + +from insteonplm.states.cover import Cover +from insteonplm.states.dimmable import ( + DimmableKeypadA, + DimmableRemote, + DimmableSwitch, + DimmableSwitch_Fan, +) +from insteonplm.states.onOff import ( + OnOffKeypad, + OnOffKeypadA, + OnOffSwitch, + OnOffSwitch_OutletBottom, + OnOffSwitch_OutletTop, + OpenClosedRelay, +) +from insteonplm.states.sensor import ( + IoLincSensor, + LeakSensorDryWet, + OnOffSensor, + SmokeCO2Sensor, + VariableSensor, +) +from insteonplm.states.x10 import ( + X10AllLightsOffSensor, + X10AllLightsOnSensor, + X10AllUnitsOffSensor, + X10DimmableSwitch, + X10OnOffSensor, + X10OnOffSwitch, +) + +State = collections.namedtuple("Product", "stateType platform") + + +class IPDB: + """Embodies the INSTEON Product Database static data and access methods.""" + + def __init__(self): + """Create the INSTEON Product Database (IPDB).""" + self.states = [ + State(Cover, "cover"), + State(OnOffSwitch_OutletTop, "switch"), + State(OnOffSwitch_OutletBottom, "switch"), + State(OpenClosedRelay, "switch"), + State(OnOffSwitch, "switch"), + State(OnOffKeypadA, "switch"), + State(OnOffKeypad, "switch"), + State(LeakSensorDryWet, "binary_sensor"), + State(IoLincSensor, "binary_sensor"), + State(SmokeCO2Sensor, "sensor"), + State(OnOffSensor, "binary_sensor"), + State(VariableSensor, "sensor"), + State(DimmableSwitch_Fan, "fan"), + State(DimmableSwitch, "light"), + State(DimmableRemote, "on_off_events"), + State(DimmableKeypadA, "light"), + State(X10DimmableSwitch, "light"), + State(X10OnOffSwitch, "switch"), + State(X10OnOffSensor, "binary_sensor"), + State(X10AllUnitsOffSensor, "binary_sensor"), + State(X10AllLightsOnSensor, "binary_sensor"), + State(X10AllLightsOffSensor, "binary_sensor"), + ] + + def __len__(self): + """Return the number of INSTEON state types mapped to HA platforms.""" + return len(self.states) + + def __iter__(self): + """Itterate through the INSTEON state types to HA platforms.""" + for product in self.states: + yield product + + def __getitem__(self, key): + """Return a Home Assistant platform from an INSTEON state type.""" + for state in self.states: + if isinstance(key, state.stateType): + return state + return None diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 3a44d89add0..60a27b3acb8 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py new file mode 100644 index 00000000000..1ae4ebed99e --- /dev/null +++ b/homeassistant/components/insteon/schemas.py @@ -0,0 +1,153 @@ +"""Schemas used by insteon component.""" + +from typing import Dict + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ADDRESS, + CONF_ENTITY_ID, + CONF_HOST, + CONF_PLATFORM, + CONF_PORT, + ENTITY_MATCH_ALL, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_FIRMWARE, + CONF_HOUSECODE, + CONF_HUB_PASSWORD, + CONF_HUB_USERNAME, + CONF_HUB_VERSION, + CONF_IP_PORT, + CONF_OVERRIDE, + CONF_PLM_HUB_MSG, + CONF_PRODUCT_KEY, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + CONF_X10_ALL_LIGHTS_OFF, + CONF_X10_ALL_LIGHTS_ON, + CONF_X10_ALL_UNITS_OFF, + DOMAIN, + HOUSECODES, + SRV_ALL_LINK_GROUP, + SRV_ALL_LINK_MODE, + SRV_CONTROLLER, + SRV_HOUSECODE, + SRV_LOAD_DB_RELOAD, + SRV_RESPONDER, +) + + +def set_default_port(schema: Dict) -> Dict: + """Set the default port based on the Hub version.""" + # If the ip_port is found do nothing + # If it is not found the set the default + ip_port = schema.get(CONF_IP_PORT) + if not ip_port: + hub_version = schema.get(CONF_HUB_VERSION) + # Found hub_version but not ip_port + if hub_version == 1: + schema[CONF_IP_PORT] = 9761 + else: + schema[CONF_IP_PORT] = 25105 + return schema + + +CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( + cv.deprecated(CONF_PLATFORM), + vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_CAT): cv.byte, + vol.Optional(CONF_SUBCAT): cv.byte, + vol.Optional(CONF_FIRMWARE): cv.byte, + vol.Optional(CONF_PRODUCT_KEY): cv.byte, + vol.Optional(CONF_PLATFORM): cv.string, + } + ), +) + + +CONF_X10_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOUSECODE): cv.string, + vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), + vol.Required(CONF_PLATFORM): cv.string, + vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255), + } + ) +) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + vol.Schema( + { + vol.Exclusive( + CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG + ): cv.string, + vol.Exclusive( + CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG + ): cv.string, + vol.Optional(CONF_IP_PORT): cv.port, + vol.Optional(CONF_HUB_USERNAME): cv.string, + vol.Optional(CONF_HUB_PASSWORD): cv.string, + vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), + vol.Optional(CONF_OVERRIDE): vol.All( + cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA] + ), + vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10): vol.All( + cv.ensure_list_csv, [CONF_X10_SCHEMA] + ), + }, + extra=vol.ALLOW_EXTRA, + required=True, + ), + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + set_default_port, + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +ADD_ALL_LINK_SCHEMA = vol.Schema( + { + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), + } +) + + +DEL_ALL_LINK_SCHEMA = vol.Schema( + {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} +) + + +LOAD_ALDB_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), + vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, + } +) + + +PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) + + +X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)}) + + +TRIGGER_SCENE_SCHEMA = vol.Schema( + {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} +) diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py index 0e8a592b92d..475723b105d 100644 --- a/homeassistant/components/insteon/sensor.py +++ b/homeassistant/components/insteon/sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity import Entity -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index c36e60c2eff..eec7874c7fb 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.switch import SwitchDevice -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py new file mode 100644 index 00000000000..9a44566bb4a --- /dev/null +++ b/homeassistant/components/insteon/utils.py @@ -0,0 +1,239 @@ +"""Utilities used by insteon component.""" + +import logging + +from insteonplm.devices import ALDBStatus + +from homeassistant.const import CONF_ADDRESS, CONF_ENTITY_ID, ENTITY_MATCH_ALL +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import ( + BUTTON_PRESSED_STATE_NAME, + DOMAIN, + EVENT_BUTTON_OFF, + EVENT_BUTTON_ON, + EVENT_CONF_BUTTON, + INSTEON_ENTITIES, + SIGNAL_LOAD_ALDB, + SIGNAL_PRINT_ALDB, + SRV_ADD_ALL_LINK, + SRV_ALL_LINK_GROUP, + SRV_ALL_LINK_MODE, + SRV_CONTROLLER, + SRV_DEL_ALL_LINK, + SRV_HOUSECODE, + SRV_LOAD_ALDB, + SRV_LOAD_DB_RELOAD, + SRV_PRINT_ALDB, + SRV_PRINT_IM_ALDB, + SRV_SCENE_OFF, + SRV_SCENE_ON, + SRV_X10_ALL_LIGHTS_OFF, + SRV_X10_ALL_LIGHTS_ON, + SRV_X10_ALL_UNITS_OFF, +) +from .ipdb import IPDB +from .schemas import ( + ADD_ALL_LINK_SCHEMA, + DEL_ALL_LINK_SCHEMA, + LOAD_ALDB_SCHEMA, + PRINT_ALDB_SCHEMA, + TRIGGER_SCENE_SCHEMA, + X10_HOUSECODE_SCHEMA, +) + +_LOGGER = logging.getLogger(__name__) + + +def register_new_device_callback(hass, config, insteon_modem): + """Register callback for new Insteon device.""" + + def _fire_button_on_off_event(address, group, val): + # Firing an event when a button is pressed. + device = insteon_modem.devices[address.hex] + state_name = device.states[group].name + button = ( + "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower() + ) + schema = {CONF_ADDRESS: address.hex} + if button != "": + schema[EVENT_CONF_BUTTON] = button + if val: + event = EVENT_BUTTON_ON + else: + event = EVENT_BUTTON_OFF + _LOGGER.debug( + "Firing event %s with address %s and button %s", event, address.hex, button + ) + hass.bus.fire(event, schema) + + @callback + def async_new_insteon_device(device): + """Detect device from transport to be delegated to platform.""" + ipdb = IPDB() + for state_key in device.states: + platform_info = ipdb[device.states[state_key]] + if platform_info and platform_info.platform: + platform = platform_info.platform + + if platform == "on_off_events": + device.states[state_key].register_updates(_fire_button_on_off_event) + + else: + _LOGGER.info( + "New INSTEON device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform, + ) + + hass.async_create_task( + discovery.async_load_platform( + hass, + platform, + DOMAIN, + discovered={ + "address": device.address.id, + "state_key": state_key, + }, + hass_config=config, + ) + ) + + insteon_modem.devices.add_device_callback(async_new_insteon_device) + + +@callback +def async_register_services(hass, config, insteon_modem): + """Register services used by insteon component.""" + + def add_all_link(service): + """Add an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + mode = service.data.get(SRV_ALL_LINK_MODE) + link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 + insteon_modem.start_all_linking(link_mode, group) + + def del_all_link(service): + """Delete an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.start_all_linking(255, group) + + def load_aldb(service): + """Load the device All-Link database.""" + entity_id = service.data[CONF_ENTITY_ID] + reload = service.data[SRV_LOAD_DB_RELOAD] + if entity_id.lower() == ENTITY_MATCH_ALL: + for entity_id in hass.data[DOMAIN][INSTEON_ENTITIES]: + _send_load_aldb_signal(entity_id, reload) + else: + _send_load_aldb_signal(entity_id, reload) + + def _send_load_aldb_signal(entity_id, reload): + """Send the load All-Link database signal to INSTEON entity.""" + signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" + dispatcher_send(hass, signal, reload) + + def print_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + entity_id = service.data[CONF_ENTITY_ID] + signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" + dispatcher_send(hass, signal) + + def print_im_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + print_aldb_to_log(insteon_modem.aldb) + + def x10_all_units_off(service): + """Send the X10 All Units Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + insteon_modem.x10_all_units_off(housecode) + + def x10_all_lights_off(service): + """Send the X10 All Lights Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + insteon_modem.x10_all_lights_off(housecode) + + def x10_all_lights_on(service): + """Send the X10 All Lights On command.""" + housecode = service.data.get(SRV_HOUSECODE) + insteon_modem.x10_all_lights_on(housecode) + + def scene_on(service): + """Trigger an INSTEON scene ON.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.trigger_group_on(group) + + def scene_off(service): + """Trigger an INSTEON scene ON.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.trigger_group_off(group) + + hass.services.async_register( + DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA + ) + hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_UNITS_OFF, x10_all_units_off, schema=X10_HOUSECODE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_LIGHTS_OFF, x10_all_lights_off, schema=X10_HOUSECODE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_LIGHTS_ON, x10_all_lights_on, schema=X10_HOUSECODE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA + ) + _LOGGER.debug("Insteon Services registered") + + +def print_aldb_to_log(aldb): + """Print the All-Link Database to the log file.""" + _LOGGER.info("ALDB load status is %s", aldb.status.name) + if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: + _LOGGER.warning("Device All-Link database not loaded") + _LOGGER.warning("Use service insteon.load_aldb first") + return + + _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") + _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") + for mem_addr in aldb: + rec = aldb[mem_addr] + # For now we write this to the log + # Roadmap is to create a configuration panel + in_use = "Y" if rec.control_flags.is_in_use else "N" + mode = "C" if rec.control_flags.is_controller else "R" + hwm = "Y" if rec.control_flags.is_high_water_mark else "N" + _LOGGER.info( + " {:04x} {:s} {:s} {:s} {:3d} {:s}" + " {:3d} {:3d} {:3d}".format( + rec.mem_addr, + in_use, + mode, + hwm, + rec.group, + rec.address.human, + rec.data1, + rec.data2, + rec.data3, + ) + ) From 981d963554b4384954203c672f4dd1baf223ec9b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jan 2020 13:08:37 +0100 Subject: [PATCH 007/378] Upgrade pre-commit to 2.0.1 (#31308) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b8ab2c23040..8d1a8ba287e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 mypy==0.761 -pre-commit==2.0.0 +pre-commit==2.0.1 pylint==2.4.4 astroid==2.3.3 pylint-strict-informational==0.1 From 24f4f53f16655da9688c3f8aa0249a9bc77b49ec Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 30 Jan 2020 10:04:06 -0500 Subject: [PATCH 008/378] ZHA dependencies bump (#31295) * ZHA dependencies bump. * Bump bellows-homeassistant. --- 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 b436f677f6b..759cb4489fe 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.12.0", + "bellows-homeassistant==0.13.1", "zha-quirks==0.0.31", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.12.0", - "zigpy-xbee-homeassistant==0.8.0", + "zigpy-homeassistant==0.13.0", + "zigpy-xbee-homeassistant==0.9.0", "zigpy-zigate==0.5.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 576305196d3..49f6b6d4c71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -299,7 +299,7 @@ beautifulsoup4==4.8.2 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.12.0 +bellows-homeassistant==0.13.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -2130,10 +2130,10 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.12.0 +zigpy-homeassistant==0.13.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.8.0 +zigpy-xbee-homeassistant==0.9.0 # homeassistant.components.zha zigpy-zigate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21df9279c89..9e1c13297a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.12.0 +bellows-homeassistant==0.13.1 # homeassistant.components.bom bomradarloop==0.1.3 @@ -699,10 +699,10 @@ zha-quirks==0.0.31 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.12.0 +zigpy-homeassistant==0.13.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.8.0 +zigpy-xbee-homeassistant==0.9.0 # homeassistant.components.zha zigpy-zigate==0.5.1 From d24e397a804c8bb0f8086d977df818d9262bf1da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 09:28:06 -0800 Subject: [PATCH 009/378] Handle service calls that do not refer entity IDs (#31317) --- homeassistant/helpers/script.py | 4 ++++ tests/helpers/test_script.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 378a6016c20..1cac4679d82 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -201,6 +201,10 @@ class Script: continue entity_ids = data.get(ATTR_ENTITY_ID) + + if entity_ids is None: + continue + if isinstance(entity_ids, str): entity_ids = [entity_ids] diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b226ed15720..5e748e3adfe 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1043,6 +1043,7 @@ async def test_referenced_entities(): "entity_id": "sensor.condition", "state": "100", }, + {"service": "test.script", "data": {"without": "entity_id"}}, {"scene": "scene.hello"}, {"event": "test_event"}, {"delay": "{{ delay_period }}"}, From 0a1e3971197bd0c10e23f9426d7a4ad6c071c70c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 30 Jan 2020 18:30:59 +0100 Subject: [PATCH 010/378] Updated frontend to 20200130.0 (#31318) --- 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 7dfcca4f019..6b16970c675 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200129.0" + "home-assistant-frontend==20200130.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 22b6328a0db..7ce2d357f82 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200129.0 +home-assistant-frontend==20200130.0 importlib-metadata==1.4.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 49f6b6d4c71..d8bf0d47386 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200129.0 +home-assistant-frontend==20200130.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e1c13297a1..137ca3ae9ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200129.0 +home-assistant-frontend==20200130.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From 33361f85804865a643ffeda777da9def21d0edf8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 09:47:16 -0800 Subject: [PATCH 011/378] Fix HTTP config serialization (#31319) --- homeassistant/components/http/__init__.py | 11 ++++++++++- tests/components/http/test_init.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 58cfb4b9cc1..565f84fdb8a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -166,7 +166,16 @@ async def async_setup(hass, config): # If we are set up successful, we store the HTTP settings for safe mode. store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - await store.async_save(conf) + + if CONF_TRUSTED_PROXIES in conf: + conf_to_save = dict(conf) + conf_to_save[CONF_TRUSTED_PROXIES] = [ + str(ip.network_address) for ip in conf_to_save[CONF_TRUSTED_PROXIES] + ] + else: + conf_to_save = conf + + await store.async_save(conf_to_save) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 43a39302f4f..58e6d8824dd 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" +from ipaddress import ip_network import logging import unittest from unittest.mock import patch @@ -244,12 +245,16 @@ async def test_cors_defaults(hass): async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): """Test that we store last working config.""" - config = {http.CONF_SERVER_PORT: aiohttp_unused_port()} + config = { + http.CONF_SERVER_PORT: aiohttp_unused_port(), + "use_x_forwarded_for": True, + "trusted_proxies": ["192.168.1.100"], + } - await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) + assert await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) await hass.async_start() + restored = await hass.components.http.async_get_last_config() + restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) - assert await hass.components.http.async_get_last_config() == http.HTTP_SCHEMA( - config - ) + assert restored == http.HTTP_SCHEMA(config) From 9432054066c0e603f00ee9be3ee9602c1929d145 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 30 Jan 2020 20:21:51 +0200 Subject: [PATCH 012/378] Rework Mikrotik device scanning following Unifi (#27484) * rework device scanning, add tests * update requirements and coverage * fix description comments * update tests, fix disabled entity updates * rework device scanning, add tests * update requirements and coverage * fix description comments * update tests, fix disabled entity updates * update librouteros to 3.0.0 * fix sorting * fix sorting 2 * revert to 2.3.0 as 3.0.0 requires code update * fix requirements * apply fixes * fix tests * update hub.py and fix tests * fix test_hub_setup_failed * rebased on dev and update librouteros to 3.0.0 * fixed test_config_flow * fixed tests * fix test_config_flow --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/mikrotik/.translations/en.json | 37 ++ homeassistant/components/mikrotik/__init__.py | 187 ++------ .../components/mikrotik/config_flow.py | 120 +++++ homeassistant/components/mikrotik/const.py | 40 +- .../components/mikrotik/device_tracker.py | 301 ++++++------- homeassistant/components/mikrotik/errors.py | 10 + homeassistant/components/mikrotik/hub.py | 413 ++++++++++++++++++ .../components/mikrotik/manifest.json | 13 +- .../components/mikrotik/strings.json | 37 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/mikrotik/__init__.py | 133 ++++++ tests/components/mikrotik/test_config_flow.py | 208 +++++++++ .../mikrotik/test_device_tracker.py | 118 +++++ tests/components/mikrotik/test_hub.py | 179 ++++++++ tests/components/mikrotik/test_init.py | 83 ++++ 18 files changed, 1546 insertions(+), 341 deletions(-) create mode 100644 homeassistant/components/mikrotik/.translations/en.json create mode 100644 homeassistant/components/mikrotik/config_flow.py create mode 100644 homeassistant/components/mikrotik/errors.py create mode 100644 homeassistant/components/mikrotik/hub.py create mode 100644 homeassistant/components/mikrotik/strings.json create mode 100644 tests/components/mikrotik/__init__.py create mode 100644 tests/components/mikrotik/test_config_flow.py create mode 100644 tests/components/mikrotik/test_device_tracker.py create mode 100644 tests/components/mikrotik/test_hub.py create mode 100644 tests/components/mikrotik/test_init.py diff --git a/.coveragerc b/.coveragerc index b936c9c514c..693959684f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -420,7 +420,8 @@ omit = homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py - homeassistant/components/mikrotik/* + homeassistant/components/mikrotik/hub.py + homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/minio/* diff --git a/CODEOWNERS b/CODEOWNERS index cbf4f3ad1e9..6983d13fc8b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,6 +209,7 @@ homeassistant/components/met/* @danielhiversen homeassistant/components/meteo_france/* @victorcerutti @oncleben31 homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel +homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff homeassistant/components/minio/* @tkislan diff --git a/homeassistant/components/mikrotik/.translations/en.json b/homeassistant/components/mikrotik/.translations/en.json new file mode 100644 index 00000000000..590563993d6 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Mikrotik", + "step": { + "user": { + "title": "Set up Mikrotik Router", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "verify_ssl": "Use ssl" + } + } + }, + "error": { + "name_exists": "Name exists", + "cannot_connect": "Connection Unsuccessful", + "wrong_credentials": "Wrong Credentials" + }, + "abort": { + "already_configured": "Mikrotik is already configured" + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Enable ARP ping", + "force_dhcp": "Force scanning using DHCP", + "detection_time": "Consider home interval" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 8c21b2e1c35..9a8ee7bdb45 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,43 +1,28 @@ -"""The mikrotik component.""" -import logging -import ssl - -from librouteros import connect -from librouteros.exceptions import LibRouterosError -from librouteros.login import plain as login_plain, token as login_token +"""The Mikrotik component.""" import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, - CONF_METHOD, + CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SSL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import load_platform from .const import ( + ATTR_MANUFACTURER, CONF_ARP_PING, - CONF_ENCODING, - CONF_LOGIN_METHOD, - CONF_TRACK_DEVICES, - DEFAULT_ENCODING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_API_PORT, + DEFAULT_DETECTION_TIME, + DEFAULT_NAME, DOMAIN, - HOSTS, - IDENTITY, - MIKROTIK_SERVICES, - MTK_LOGIN_PLAIN, - MTK_LOGIN_TOKEN, - NAME, ) - -_LOGGER = logging.getLogger(__name__) - -MTK_DEFAULT_API_PORT = "8728" -MTK_DEFAULT_API_SSL_PORT = "8729" +from .hub import MikrotikHub MIKROTIK_SCHEMA = vol.All( vol.Schema( @@ -45,13 +30,14 @@ MIKROTIK_SCHEMA = vol.All( vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_METHOD): cv.string, - vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN), - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, - vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_API_PORT): cv.port, + vol.Optional(CONF_VERIFY_SSL, default=False): cv.boolean, vol.Optional(CONF_ARP_PING, default=False): cv.boolean, + vol.Optional(CONF_FORCE_DHCP, default=False): cv.boolean, + vol.Optional( + CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME + ): cv.time_period, } ) ) @@ -61,124 +47,45 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up the Mikrotik component.""" - hass.data[DOMAIN] = {HOSTS: {}} +async def async_setup(hass, config): + """Import the Mikrotik component from config.""" - for device in config[DOMAIN]: - host = device[CONF_HOST] - use_ssl = device.get(CONF_SSL) - user = device.get(CONF_USERNAME) - password = device.get(CONF_PASSWORD, "") - login = device.get(CONF_LOGIN_METHOD) - encoding = device.get(CONF_ENCODING) - track_devices = device.get(CONF_TRACK_DEVICES) - - if CONF_PORT in device: - port = device.get(CONF_PORT) - else: - if use_ssl: - port = MTK_DEFAULT_API_SSL_PORT - else: - port = MTK_DEFAULT_API_PORT - - if login == MTK_LOGIN_PLAIN: - login_method = login_plain - else: - login_method = login_token - - try: - api = MikrotikClient( - host, use_ssl, port, user, password, login_method, encoding + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) ) - api.connect_to_device() - hass.data[DOMAIN][HOSTS][host] = {"config": device, "api": api} - except LibRouterosError as api_error: - _LOGGER.error("Mikrotik %s error %s", host, api_error) - continue - if track_devices: - hass.data[DOMAIN][HOSTS][host][DEVICE_TRACKER] = True - load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config) - - if not hass.data[DOMAIN][HOSTS]: - return False return True -class MikrotikClient: - """Handle all communication with the Mikrotik API.""" +async def async_setup_entry(hass, config_entry): + """Set up the Mikrotik component.""" - def __init__(self, host, use_ssl, port, user, password, login_method, encoding): - """Initialize the Mikrotik Client.""" - self._host = host - self._use_ssl = use_ssl - self._port = port - self._user = user - self._password = password - self._login_method = login_method - self._encoding = encoding - self._ssl_wrapper = None - self.hostname = None - self._client = None - self._connected = False + hub = MikrotikHub(hass, config_entry) + if not await hub.async_setup(): + return False - def connect_to_device(self): - """Connect to Mikrotik device.""" - self._connected = False - _LOGGER.debug("[%s] Connecting to Mikrotik device", self._host) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(DOMAIN, hub.serial_num)}, + manufacturer=ATTR_MANUFACTURER, + model=hub.model, + name=hub.hostname, + sw_version=hub.firmware, + ) - kwargs = { - "encoding": self._encoding, - "login_methods": self._login_method, - "port": self._port, - } + return True - if self._use_ssl: - if self._ssl_wrapper is None: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - self._ssl_wrapper = ssl_context.wrap_socket - kwargs["ssl_wrapper"] = self._ssl_wrapper - try: - self._client = connect(self._host, self._user, self._password, **kwargs) - self._connected = True - except LibRouterosError as api_error: - _LOGGER.error("Mikrotik %s: %s", self._host, api_error) - self._client = None - return False +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") - self.hostname = self.get_hostname() - _LOGGER.info("Mikrotik Connected to %s (%s)", self.hostname, self._host) - return self._connected + hass.data[DOMAIN].pop(config_entry.entry_id) - def get_hostname(self): - """Return device host name.""" - data = list(self.command(MIKROTIK_SERVICES[IDENTITY])) - return data[0][NAME] if data else None - - def connected(self): - """Return connected boolean.""" - return self._connected - - def command(self, cmd, params=None): - """Retrieve data from Mikrotik API.""" - if not self._connected or not self._client: - if not self.connect_to_device(): - return None - try: - if params: - response = self._client(cmd=cmd, **params) - else: - response = self._client(cmd=cmd) - except LibRouterosError as api_error: - _LOGGER.error( - "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", - self._host, - cmd, - api_error, - ) - return None - return response if response else None + return True diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py new file mode 100644 index 00000000000..c1a41abf0d0 --- /dev/null +++ b/homeassistant/components/mikrotik/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow for Mikrotik.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback + +from .const import ( + CONF_ARP_PING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_API_PORT, + DEFAULT_DETECTION_TIME, + DEFAULT_NAME, + DOMAIN, +) +from .errors import CannotConnect, LoginError +from .hub import get_api + + +class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Mikrotik config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return MikrotikOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + errors[CONF_NAME] = "name_exists" + break + + try: + await self.hass.async_add_executor_job(get_api, self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except LoginError: + errors[CONF_USERNAME] = "wrong_credentials" + errors[CONF_PASSWORD] = "wrong_credentials" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_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_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_API_PORT): int, + vol.Optional(CONF_VERIFY_SSL, default=False): bool, + } + ), + errors=errors, + ) + + async def async_step_import(self, import_config): + """Import Miktortik from config.""" + + import_config[CONF_DETECTION_TIME] = import_config[CONF_DETECTION_TIME].seconds + return await self.async_step_user(user_input=import_config) + + +class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Mikrotik options.""" + + def __init__(self, config_entry): + """Initialize Mikrotik options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Mikrotik options.""" + return await self.async_step_device_tracker() + + async def async_step_device_tracker(self, user_input=None): + """Manage the device tracker options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_FORCE_DHCP, + default=self.config_entry.options.get(CONF_FORCE_DHCP, False), + ): bool, + vol.Optional( + CONF_ARP_PING, + default=self.config_entry.options.get(CONF_ARP_PING, False), + ): bool, + vol.Optional( + CONF_DETECTION_TIME, + default=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + ): int, + } + + return self.async_show_form( + step_id="device_tracker", data_schema=vol.Schema(options) + ) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index bd26b02fe1b..d66a441aaf7 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,32 +1,38 @@ """Constants used in the Mikrotik components.""" DOMAIN = "mikrotik" -MIKROTIK = DOMAIN -HOSTS = "hosts" -MTK_LOGIN_PLAIN = "plain" -MTK_LOGIN_TOKEN = "token" +DEFAULT_NAME = "Mikrotik" +DEFAULT_API_PORT = 8728 +DEFAULT_DETECTION_TIME = 300 + +ATTR_MANUFACTURER = "Mikrotik" +ATTR_SERIAL_NUMBER = "serial-number" +ATTR_FIRMWARE = "current-firmware" +ATTR_MODEL = "model" CONF_ARP_PING = "arp_ping" -CONF_TRACK_DEVICES = "track_devices" -CONF_LOGIN_METHOD = "login_method" -CONF_ENCODING = "encoding" -DEFAULT_ENCODING = "utf-8" +CONF_FORCE_DHCP = "force_dhcp" +CONF_DETECTION_TIME = "detection_time" + NAME = "name" INFO = "info" IDENTITY = "identity" ARP = "arp" + +CAPSMAN = "capsman" DHCP = "dhcp" WIRELESS = "wireless" -CAPSMAN = "capsman" +IS_WIRELESS = "is_wireless" MIKROTIK_SERVICES = { - INFO: "/system/routerboard/getall", - IDENTITY: "/system/identity/getall", ARP: "/ip/arp/getall", - DHCP: "/ip/dhcp-server/lease/getall", - WIRELESS: "/interface/wireless/registration-table/getall", CAPSMAN: "/caps-man/registration-table/getall", + DHCP: "/ip/dhcp-server/lease/getall", + IDENTITY: "/system/identity/getall", + INFO: "/system/routerboard/getall", + WIRELESS: "/interface/wireless/registration-table/getall", + IS_WIRELESS: "/interface/wireless/print", } ATTR_DEVICE_TRACKER = [ @@ -34,16 +40,8 @@ ATTR_DEVICE_TRACKER = [ "mac-address", "ssid", "interface", - "host-name", - "last-seen", - "rx-signal", "signal-strength", - "tx-ccq", "signal-to-noise", - "wmm-enabled", - "authentication-type", - "encryption", - "tx-rate-set", "rx-rate", "tx-rate", "uptime", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 92fcfac4ae4..e7c5e5655a0 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,191 +1,142 @@ """Support for Mikrotik routers as device tracker.""" import logging -from homeassistant.components.device_tracker import ( +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) -from homeassistant.const import CONF_METHOD -from homeassistant.util import slugify +from homeassistant.core import callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util -from .const import ( - ARP, - ATTR_DEVICE_TRACKER, - CAPSMAN, - CONF_ARP_PING, - DHCP, - HOSTS, - MIKROTIK, - MIKROTIK_SERVICES, - WIRELESS, -) +from .const import ATTR_MANUFACTURER, DOMAIN _LOGGER = logging.getLogger(__name__) -def get_scanner(hass, config): - """Validate the configuration and return MikrotikScanner.""" - for host in hass.data[MIKROTIK][HOSTS]: - if DEVICE_TRACKER not in hass.data[MIKROTIK][HOSTS][host]: - continue - hass.data[MIKROTIK][HOSTS][host].pop(DEVICE_TRACKER, None) - api = hass.data[MIKROTIK][HOSTS][host]["api"] - config = hass.data[MIKROTIK][HOSTS][host]["config"] - hostname = api.get_hostname() - scanner = MikrotikScanner(api, host, hostname, config) - return scanner if scanner.success_init else None +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up device tracker for Mikrotik component.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + tracked = {} + + registry = await entity_registry.async_get_registry(hass) + + # Restore clients that is not a part of active clients list. + for entity in registry.entities.values(): + + if ( + entity.config_entry_id == config_entry.entry_id + and entity.domain == DEVICE_TRACKER + ): + + if ( + entity.unique_id in hub.api.devices + or entity.unique_id not in hub.api.all_devices + ): + continue + hub.api.restore_device(entity.unique_id) + + @callback + def update_hub(): + """Update the status of the device.""" + update_items(hub, async_add_entities, tracked) + + async_dispatcher_connect(hass, hub.signal_update, update_hub) + + update_hub() -class MikrotikScanner(DeviceScanner): - """This class queries a Mikrotik device.""" +@callback +def update_items(hub, async_add_entities, tracked): + """Update tracked device state from the hub.""" + new_tracked = [] + for mac, device in hub.api.devices.items(): + if mac not in tracked: + tracked[mac] = MikrotikHubTracker(device, hub) + new_tracked.append(tracked[mac]) - def __init__(self, api, host, hostname, config): - """Initialize the scanner.""" - self.api = api - self.config = config - self.host = host - self.hostname = hostname - self.method = config.get(CONF_METHOD) - self.arp_ping = config.get(CONF_ARP_PING) - self.dhcp = None - self.devices_arp = {} - self.devices_dhcp = {} - self.device_tracker = None - self.success_init = self.api.connected() - - def get_extra_attributes(self, device): - """ - Get extra attributes of a device. - - Some known extra attributes that may be returned in the device tuple - include MAC address (mac), network device (dev), IP address - (ip), reachable status (reachable), associated router - (host), hostname if known (hostname) among others. - """ - return self.device_tracker.get(device) or {} - - def get_device_name(self, device): - """Get name for a device.""" - host = self.device_tracker.get(device, {}) - return host.get("host_name") - - def scan_devices(self): - """Scan for new devices and return a list with found device MACs.""" - self.update_device_tracker() - return list(self.device_tracker) - - def get_method(self): - """Determine the device tracker polling method.""" - if self.method: - _LOGGER.debug( - "Mikrotik %s: Manually selected polling method %s", - self.host, - self.method, - ) - return self.method - - capsman = self.api.command(MIKROTIK_SERVICES[CAPSMAN]) - if not capsman: - _LOGGER.debug( - "Mikrotik %s: Not a CAPsMAN controller. " - "Trying local wireless interfaces", - (self.host), - ) - else: - return CAPSMAN - - wireless = self.api.command(MIKROTIK_SERVICES[WIRELESS]) - if not wireless: - _LOGGER.info( - "Mikrotik %s: Wireless adapters not found. Try to " - "use DHCP lease table as presence tracker source. " - "Please decrease lease time as much as possible", - self.host, - ) - return DHCP - - return WIRELESS - - def update_device_tracker(self): - """Update device_tracker from Mikrotik API.""" - self.device_tracker = {} - if not self.method: - self.method = self.get_method() - - data = self.api.command(MIKROTIK_SERVICES[self.method]) - if data is None: - return - - if self.method != DHCP: - dhcp = self.api.command(MIKROTIK_SERVICES[DHCP]) - if dhcp is not None: - self.devices_dhcp = load_mac(dhcp) - - arp = self.api.command(MIKROTIK_SERVICES[ARP]) - self.devices_arp = load_mac(arp) - - for device in data: - mac = device.get("mac-address") - if self.method == DHCP: - if "active-address" not in device: - continue - - if self.arp_ping and self.devices_arp: - if mac not in self.devices_arp: - continue - ip_address = self.devices_arp[mac]["address"] - interface = self.devices_arp[mac]["interface"] - if not self.do_arp_ping(ip_address, interface): - continue - - attrs = {} - if mac in self.devices_dhcp and "host-name" in self.devices_dhcp[mac]: - hostname = self.devices_dhcp[mac].get("host-name") - if hostname: - attrs["host_name"] = hostname - - if self.devices_arp and mac in self.devices_arp: - attrs["ip_address"] = self.devices_arp[mac].get("address") - - for attr in ATTR_DEVICE_TRACKER: - if attr in device and device[attr] is not None: - attrs[slugify(attr)] = device[attr] - attrs["scanner_type"] = self.method - attrs["scanner_host"] = self.host - attrs["scanner_hostname"] = self.hostname - self.device_tracker[mac] = attrs - - def do_arp_ping(self, ip_address, interface): - """Attempt to arp ping MAC address via interface.""" - params = { - "arp-ping": "yes", - "interval": "100ms", - "count": 3, - "interface": interface, - "address": ip_address, - } - cmd = "/ping" - data = self.api.command(cmd, params) - if data is not None: - status = 0 - for result in data: - if "status" in result: - _LOGGER.debug( - "Mikrotik %s arp_ping error: %s", self.host, result["status"] - ) - status += 1 - if status == len(data): - return None - return data + if new_tracked: + async_add_entities(new_tracked) -def load_mac(devices=None): - """Load dictionary using MAC address as key.""" - if not devices: +class MikrotikHubTracker(ScannerEntity): + """Representation of network device.""" + + def __init__(self, device, hub): + """Initialize the tracked device.""" + self.device = device + self.hub = hub + self.unsub_dispatcher = None + + @property + def is_connected(self): + """Return true if the client is connected to the network.""" + if ( + self.device.last_seen + and (dt_util.utcnow() - self.device.last_seen) + < self.hub.option_detection_time + ): + return True + return False + + @property + def source_type(self): + """Return the source type of the client.""" + return SOURCE_TYPE_ROUTER + + @property + def name(self) -> str: + """Return the name of the client.""" + return self.device.name + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return self.device.mac + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.hub.available + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if self.is_connected: + return self.device.attrs return None - mac_devices = {} - for device in devices: - if "mac-address" in device: - mac = device.pop("mac-address") - mac_devices[mac] = device - return mac_devices + + @property + def device_info(self): + """Return a client description for device registry.""" + info = { + "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, + "manufacturer": ATTR_MANUFACTURER, + "identifiers": {(DOMAIN, self.device.mac)}, + "name": self.name, + "via_device": (DOMAIN, self.hub.serial_num), + } + return info + + async def async_added_to_hass(self): + """Client entity created.""" + _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id) + self.unsub_dispatcher = async_dispatcher_connect( + self.hass, self.hub.signal_update, self.async_write_ha_state + ) + + async def async_update(self): + """Synchronize state with hub.""" + _LOGGER.debug( + "Updating Mikrotik tracked client %s (%s)", self.entity_id, self.unique_id + ) + await self.hub.request_update() + + async def will_remove_from_hass(self): + """Disconnect from dispatcher.""" + if self.unsub_dispatcher: + self.unsub_dispatcher() diff --git a/homeassistant/components/mikrotik/errors.py b/homeassistant/components/mikrotik/errors.py new file mode 100644 index 00000000000..22cd63d7468 --- /dev/null +++ b/homeassistant/components/mikrotik/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Mikrotik component.""" +from homeassistant.exceptions import HomeAssistantError + + +class CannotConnect(HomeAssistantError): + """Unable to connect to the hub.""" + + +class LoginError(HomeAssistantError): + """Component got logged out.""" diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py new file mode 100644 index 00000000000..2243b6cc5ce --- /dev/null +++ b/homeassistant/components/mikrotik/hub.py @@ -0,0 +1,413 @@ +"""The Mikrotik router class.""" +from datetime import timedelta +import logging +import socket +import ssl + +import librouteros +from librouteros.login import plain as login_plain, token as login_token + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util import slugify +import homeassistant.util.dt as dt_util + +from .const import ( + ARP, + ATTR_DEVICE_TRACKER, + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + CAPSMAN, + CONF_ARP_PING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_DETECTION_TIME, + DHCP, + IDENTITY, + INFO, + IS_WIRELESS, + MIKROTIK_SERVICES, + NAME, + WIRELESS, +) +from .errors import CannotConnect, LoginError + +_LOGGER = logging.getLogger(__name__) + + +class Device: + """Represents a network device.""" + + def __init__(self, mac, params): + """Initialize the network device.""" + self._mac = mac + self._params = params + self._last_seen = None + self._attrs = {} + self._wireless_params = None + + @property + def name(self): + """Return device name.""" + return self._params.get("host-name", self.mac) + + @property + def mac(self): + """Return device mac.""" + return self._mac + + @property + def last_seen(self): + """Return device last seen.""" + return self._last_seen + + @property + def attrs(self): + """Return device attributes.""" + attr_data = self._wireless_params if self._wireless_params else self._params + for attr in ATTR_DEVICE_TRACKER: + if attr in attr_data: + self._attrs[slugify(attr)] = attr_data[attr] + self._attrs["ip_address"] = self._params.get("active-address") + return self._attrs + + def update(self, wireless_params=None, params=None, active=False): + """Update Device params.""" + if wireless_params: + self._wireless_params = wireless_params + if params: + self._params = params + if active: + self._last_seen = dt_util.utcnow() + + +class MikrotikData: + """Handle all communication with the Mikrotik API.""" + + def __init__(self, hass, config_entry, api): + """Initialize the Mikrotik Client.""" + self.hass = hass + self.config_entry = config_entry + self.api = api + self._host = self.config_entry.data[CONF_HOST] + self.all_devices = {} + self.devices = {} + self.available = True + self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) + self.hostname = None + self.model = None + self.firmware = None + self.serial_number = None + + @staticmethod + def load_mac(devices=None): + """Load dictionary using MAC address as key.""" + if not devices: + return None + mac_devices = {} + for device in devices: + if "mac-address" in device: + mac = device["mac-address"] + mac_devices[mac] = device + return mac_devices + + @property + def arp_enabled(self): + """Return arp_ping option setting.""" + return self.config_entry.options[CONF_ARP_PING] + + @property + def force_dhcp(self): + """Return force_dhcp option setting.""" + return self.config_entry.options[CONF_FORCE_DHCP] + + def get_info(self, param): + """Return device model name.""" + cmd = IDENTITY if param == NAME else INFO + data = list(self.command(MIKROTIK_SERVICES[cmd])) + return data[0].get(param) if data else None + + def get_hub_details(self): + """Get Hub info.""" + self.hostname = self.get_info(NAME) + self.model = self.get_info(ATTR_MODEL) + self.firmware = self.get_info(ATTR_FIRMWARE) + self.serial_number = self.get_info(ATTR_SERIAL_NUMBER) + + def connect_to_hub(self): + """Connect to hub.""" + try: + self.api = get_api(self.hass, self.config_entry.data) + self.available = True + return True + except (LoginError, CannotConnect): + self.available = False + return False + + def get_list_from_interface(self, interface): + """Get devices from interface.""" + result = list(self.command(MIKROTIK_SERVICES[interface])) + return self.load_mac(result) if result else {} + + def restore_device(self, mac): + """Restore a missing device after restart.""" + self.devices[mac] = Device(mac, self.all_devices[mac]) + + def update_devices(self): + """Get list of devices with latest status.""" + arp_devices = {} + wireless_devices = {} + device_list = {} + try: + self.all_devices = self.get_list_from_interface(DHCP) + if self.support_wireless: + _LOGGER.debug("wireless is supported") + for interface in [CAPSMAN, WIRELESS]: + wireless_devices = self.get_list_from_interface(interface) + if wireless_devices: + _LOGGER.debug("Scanning wireless devices using %s", interface) + break + + if self.support_wireless and not self.force_dhcp: + device_list = wireless_devices + else: + device_list = self.all_devices + _LOGGER.debug("Falling back to DHCP for scanning devices") + + if self.arp_enabled: + arp_devices = self.get_list_from_interface(ARP) + + # get new hub firmware version if updated + self.firmware = self.get_info(ATTR_FIRMWARE) + + except (CannotConnect, socket.timeout, socket.error): + self.available = False + return + + if not device_list: + return + + for mac, params in device_list.items(): + if mac not in self.devices: + self.devices[mac] = Device(mac, self.all_devices.get(mac, {})) + else: + self.devices[mac].update(params=self.all_devices.get(mac, {})) + + if mac in wireless_devices: + # if wireless is supported then wireless_params are params + self.devices[mac].update( + wireless_params=wireless_devices[mac], active=True + ) + continue + # for wired devices or when forcing dhcp check for active-address + if not params.get("active-address"): + self.devices[mac].update(active=False) + continue + # ping check the rest of active devices if arp ping is enabled + active = True + if self.arp_enabled and mac in arp_devices: + active = self.do_arp_ping( + params.get("active-address"), arp_devices[mac].get("interface") + ) + self.devices[mac].update(active=active) + + def do_arp_ping(self, ip_address, interface): + """Attempt to arp ping MAC address via interface.""" + _LOGGER.debug("pinging - %s", ip_address) + params = { + "arp-ping": "yes", + "interval": "100ms", + "count": 3, + "interface": interface, + "address": ip_address, + } + cmd = "/ping" + data = list(self.command(cmd, params)) + if data is not None: + status = 0 + for result in data: + if "status" in result: + status += 1 + if status == len(data): + _LOGGER.debug( + "Mikrotik %s - %s arp_ping timed out", ip_address, interface + ) + return False + return True + + def command(self, cmd, params=None): + """Retrieve data from Mikrotik API.""" + try: + _LOGGER.info("Running command %s", cmd) + if params: + response = self.api(cmd=cmd, **params) + else: + response = self.api(cmd=cmd) + except ( + librouteros.exceptions.ConnectionClosed, + socket.error, + socket.timeout, + ) as api_error: + _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) + raise CannotConnect + except librouteros.exceptions.ProtocolError as api_error: + _LOGGER.warning( + "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", + self._host, + cmd, + api_error, + ) + return None + + return response if response else None + + def update(self): + """Update device_tracker from Mikrotik API.""" + if not self.available or not self.api: + if not self.connect_to_hub(): + return + _LOGGER.debug("updating network devices for host: %s", self._host) + self.update_devices() + + +class MikrotikHub: + """Mikrotik Hub Object.""" + + def __init__(self, hass, config_entry): + """Initialize the Mikrotik Client.""" + self.hass = hass + self.config_entry = config_entry + self._mk_data = None + self.progress = None + + @property + def host(self): + """Return the host of this hub.""" + return self.config_entry.data[CONF_HOST] + + @property + def hostname(self): + """Return the hostname of the hub.""" + return self._mk_data.hostname + + @property + def model(self): + """Return the model of the hub.""" + return self._mk_data.model + + @property + def firmware(self): + """Return the firware of the hub.""" + return self._mk_data.firmware + + @property + def serial_num(self): + """Return the serial number of the hub.""" + return self._mk_data.serial_number + + @property + def available(self): + """Return if the hub is connected.""" + return self._mk_data.available + + @property + def option_detection_time(self): + """Config entry option defining number of seconds from last seen to away.""" + return timedelta(seconds=self.config_entry.options[CONF_DETECTION_TIME]) + + @property + def signal_update(self): + """Event specific per Mikrotik entry to signal updates.""" + return f"mikrotik-update-{self.host}" + + @property + def api(self): + """Represent Mikrotik data object.""" + return self._mk_data + + async def async_add_options(self): + """Populate default options for Mikrotik.""" + if not self.config_entry.options: + options = { + CONF_ARP_PING: self.config_entry.data.pop(CONF_ARP_PING, False), + CONF_FORCE_DHCP: self.config_entry.data.pop(CONF_FORCE_DHCP, False), + CONF_DETECTION_TIME: self.config_entry.data.pop( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + } + + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + + async def request_update(self): + """Request an update.""" + if self.progress is not None: + await self.progress + return + + self.progress = self.hass.async_create_task(self.async_update()) + await self.progress + + self.progress = None + + async def async_update(self): + """Update Mikrotik devices information.""" + await self.hass.async_add_executor_job(self._mk_data.update) + async_dispatcher_send(self.hass, self.signal_update) + + async def async_setup(self): + """Set up the Mikrotik hub.""" + try: + api = await self.hass.async_add_executor_job( + get_api, self.hass, self.config_entry.data + ) + except CannotConnect: + raise ConfigEntryNotReady + except LoginError: + return False + + self._mk_data = MikrotikData(self.hass, self.config_entry, api) + await self.async_add_options() + await self.hass.async_add_executor_job(self._mk_data.get_hub_details) + await self.hass.async_add_executor_job(self._mk_data.update) + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "device_tracker" + ) + ) + return True + + +def get_api(hass, entry): + """Connect to Mikrotik hub.""" + _LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST]) + + _login_method = (login_plain, login_token) + kwargs = {"login_methods": _login_method, "port": entry["port"]} + + if entry[CONF_VERIFY_SSL]: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + _ssl_wrapper = ssl_context.wrap_socket + kwargs["ssl_wrapper"] = _ssl_wrapper + + try: + api = librouteros.connect( + entry[CONF_HOST], entry[CONF_USERNAME], entry[CONF_PASSWORD], **kwargs, + ) + _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) + return api + except ( + librouteros.exceptions.LibRouterosError, + socket.error, + socket.timeout, + ) as api_error: + _LOGGER.error("Mikrotik %s error: %s", entry[CONF_HOST], api_error) + if "invalid user name or password" in str(api_error): + raise LoginError + raise CannotConnect diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 932df2edd29..72f98a11709 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -1,8 +1,13 @@ { "domain": "mikrotik", - "name": "MikroTik", + "name": "Mikrotik", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mikrotik", - "requirements": ["librouteros==3.0.0"], + "requirements": [ + "librouteros==3.0.0" + ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ] +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/strings.json b/homeassistant/components/mikrotik/strings.json new file mode 100644 index 00000000000..590563993d6 --- /dev/null +++ b/homeassistant/components/mikrotik/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Mikrotik", + "step": { + "user": { + "title": "Set up Mikrotik Router", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "verify_ssl": "Use ssl" + } + } + }, + "error": { + "name_exists": "Name exists", + "cannot_connect": "Connection Unsuccessful", + "wrong_credentials": "Wrong Credentials" + }, + "abort": { + "already_configured": "Mikrotik is already configured" + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Enable ARP ping", + "force_dhcp": "Force scanning using DHCP", + "detection_time": "Consider home interval" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 70fc4355061..cf77dae7fb2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -54,6 +54,7 @@ FLOWS = [ "luftdaten", "mailgun", "met", + "mikrotik", "mobile_app", "mqtt", "neato", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 137ca3ae9ba..5a8a794b429 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -283,6 +283,9 @@ keyrings.alt==3.4.0 # homeassistant.components.dyson libpurecool==0.6.0 +# homeassistant.components.mikrotik +librouteros==3.0.0 + # homeassistant.components.soundtouch libsoundtouch==0.7.2 diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py new file mode 100644 index 00000000000..ae8013eff4b --- /dev/null +++ b/tests/components/mikrotik/__init__.py @@ -0,0 +1,133 @@ +"""Tests for the Mikrotik component.""" +from homeassistant.components import mikrotik + +MOCK_DATA = { + mikrotik.CONF_NAME: "Mikrotik", + mikrotik.CONF_HOST: "0.0.0.0", + mikrotik.CONF_USERNAME: "user", + mikrotik.CONF_PASSWORD: "pass", + mikrotik.CONF_PORT: 8278, + mikrotik.CONF_VERIFY_SSL: False, +} + +MOCK_OPTIONS = { + mikrotik.CONF_ARP_PING: False, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.CONF_DETECTION_TIME: mikrotik.DEFAULT_DETECTION_TIME, +} + +DEVICE_1_DHCP = { + ".id": "*1A", + "address": "0.0.0.1", + "mac-address": "00:00:00:00:00:01", + "active-address": "0.0.0.1", + "host-name": "Device_1", + "comment": "Mobile", +} +DEVICE_2_DHCP = { + ".id": "*1B", + "address": "0.0.0.2", + "mac-address": "00:00:00:00:00:02", + "active-address": "0.0.0.2", + "host-name": "Device_2", + "comment": "PC", +} +DEVICE_1_WIRELESS = { + ".id": "*264", + "interface": "wlan1", + "mac-address": "00:00:00:00:00:01", + "ap": False, + "wds": False, + "bridge": False, + "rx-rate": "72.2Mbps-20MHz/1S/SGI", + "tx-rate": "72.2Mbps-20MHz/1S/SGI", + "packets": "59542,17464", + "bytes": "17536671,2966351", + "frames": "59542,17472", + "frame-bytes": "17655785,2862445", + "hw-frames": "78935,38395", + "hw-frame-bytes": "25636019,4063445", + "tx-frames-timed-out": 0, + "uptime": "5h49m36s", + "last-activity": "170ms", + "signal-strength": "-62@1Mbps", + "signal-to-noise": 52, + "signal-strength-ch0": -63, + "signal-strength-ch1": -69, + "strength-at-rates": "-62@1Mbps 16s330ms,-64@6Mbps 13s560ms,-65@HT20-3 52m6s30ms,-66@HT20-4 52m4s350ms,-66@HT20-5 51m58s580ms,-65@HT20-6 51m24s780ms,-65@HT20-7 5s680ms", + "tx-ccq": 93, + "p-throughput": 54928, + "last-ip": "0.0.0.1", + "802.1x-port-enabled": True, + "authentication-type": "wpa2-psk", + "encryption": "aes-ccm", + "group-encryption": "aes-ccm", + "management-protection": False, + "wmm-enabled": True, + "tx-rate-set": "OFDM:6-54 BW:1x SGI:1x HT:0-7", +} + +DEVICE_2_WIRELESS = { + ".id": "*265", + "interface": "wlan1", + "mac-address": "00:00:00:00:00:02", + "ap": False, + "wds": False, + "bridge": False, + "rx-rate": "72.2Mbps-20MHz/1S/SGI", + "tx-rate": "72.2Mbps-20MHz/1S/SGI", + "packets": "59542,17464", + "bytes": "17536671,2966351", + "frames": "59542,17472", + "frame-bytes": "17655785,2862445", + "hw-frames": "78935,38395", + "hw-frame-bytes": "25636019,4063445", + "tx-frames-timed-out": 0, + "uptime": "5h49m36s", + "last-activity": "170ms", + "signal-strength": "-62@1Mbps", + "signal-to-noise": 52, + "signal-strength-ch0": -63, + "signal-strength-ch1": -69, + "strength-at-rates": "-62@1Mbps 16s330ms,-64@6Mbps 13s560ms,-65@HT20-3 52m6s30ms,-66@HT20-4 52m4s350ms,-66@HT20-5 51m58s580ms,-65@HT20-6 51m24s780ms,-65@HT20-7 5s680ms", + "tx-ccq": 93, + "p-throughput": 54928, + "last-ip": "0.0.0.2", + "802.1x-port-enabled": True, + "authentication-type": "wpa2-psk", + "encryption": "aes-ccm", + "group-encryption": "aes-ccm", + "management-protection": False, + "wmm-enabled": True, + "tx-rate-set": "OFDM:6-54 BW:1x SGI:1x HT:0-7", +} +DHCP_DATA = [DEVICE_1_DHCP, DEVICE_2_DHCP] + +WIRELESS_DATA = [DEVICE_1_WIRELESS] + +ARP_DATA = [ + { + ".id": "*1", + "address": "0.0.0.1", + "mac-address": "00:00:00:00:00:01", + "interface": "bridge", + "published": False, + "invalid": False, + "DHCP": True, + "dynamic": True, + "complete": True, + "disabled": False, + }, + { + ".id": "*2", + "address": "0.0.0.2", + "mac-address": "00:00:00:00:00:02", + "interface": "bridge", + "published": False, + "invalid": False, + "DHCP": True, + "dynamic": True, + "complete": True, + "disabled": False, + }, +] diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py new file mode 100644 index 00000000000..25f541e9287 --- /dev/null +++ b/tests/components/mikrotik/test_config_flow.py @@ -0,0 +1,208 @@ +"""Test Mikrotik setup process.""" +from datetime import timedelta +from unittest.mock import patch + +import librouteros +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components import mikrotik +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + +DEMO_USER_INPUT = { + CONF_NAME: "Home router", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, +} + +DEMO_CONFIG = { + CONF_NAME: "Home router", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.CONF_ARP_PING: False, + mikrotik.CONF_DETECTION_TIME: timedelta(seconds=30), +} + +DEMO_CONFIG_ENTRY = { + CONF_NAME: "Home router", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.CONF_ARP_PING: False, + mikrotik.CONF_DETECTION_TIME: 30, +} + + +@pytest.fixture(name="api") +def mock_mikrotik_api(): + """Mock an api.""" + with patch("librouteros.connect"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "librouteros.connect", + side_effect=librouteros.exceptions.TrapError("invalid user name or password"), + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "librouteros.connect", side_effect=librouteros.exceptions.ConnectionClosed + ): + yield + + +async def test_import(hass, api): + """Test import step.""" + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "import"}, data=DEMO_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Home router" + assert result["data"][CONF_NAME] == "Home router" + assert result["data"][CONF_HOST] == "0.0.0.0" + assert result["data"][CONF_USERNAME] == "username" + assert result["data"][CONF_PASSWORD] == "password" + assert result["data"][CONF_PORT] == 8278 + assert result["data"][CONF_VERIFY_SSL] is False + + +async def test_flow_works(hass, api): + """Test config flow.""" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Home router" + assert result["data"][CONF_NAME] == "Home router" + assert result["data"][CONF_HOST] == "0.0.0.0" + assert result["data"][CONF_USERNAME] == "username" + assert result["data"][CONF_PASSWORD] == "password" + assert result["data"][CONF_PORT] == 8278 + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device_tracker" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mikrotik.CONF_DETECTION_TIME: 30, + mikrotik.CONF_ARP_PING: True, + mikrotik.const.CONF_FORCE_DHCP: False, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + mikrotik.CONF_DETECTION_TIME: 30, + mikrotik.CONF_ARP_PING: True, + mikrotik.const.CONF_FORCE_DHCP: False, + } + + +async def test_host_already_configured(hass, auth_error): + """Test host already configured.""" + + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_name_exists(hass, api): + """Test name already configured.""" + + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry.add_to_hass(hass) + user_input = DEMO_USER_INPUT.copy() + user_input[CONF_HOST] = "0.0.0.1" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + + assert result["type"] == "form" + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_connection_error(hass, conn_error): + """Test error when connection is unsuccesful.""" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_wrong_credentials(hass, auth_error): + """Test error when credentials are wrong.""" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == { + CONF_USERNAME: "wrong_credentials", + CONF_PASSWORD: "wrong_credentials", + } diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py new file mode 100644 index 00000000000..643f94a5ad5 --- /dev/null +++ b/tests/components/mikrotik/test_device_tracker.py @@ -0,0 +1,118 @@ +"""The tests for the Mikrotik device tracker platform.""" +from datetime import timedelta + +from homeassistant.components import mikrotik +import homeassistant.components.device_tracker as device_tracker +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import DEVICE_2_WIRELESS, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA +from .test_hub import setup_mikrotik_entry + +from tests.common import MockConfigEntry, patch + +DEFAULT_DETECTION_TIME = timedelta(seconds=300) + + +def mock_command(self, cmd, params=None): + """Mock the Mikrotik command method.""" + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return True + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return DHCP_DATA + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return WIRELESS_DATA + return {} + + +async def test_platform_manually_configured(hass): + """Test that nothing happens when configuring mikrotik through device tracker platform.""" + assert ( + await async_setup_component( + hass, + device_tracker.DOMAIN, + {device_tracker.DOMAIN: {"platform": "mikrotik"}}, + ) + is False + ) + assert mikrotik.DOMAIN not in hass.data + + +async def test_device_trackers(hass): + """Test device_trackers created by mikrotik.""" + + # test devices are added from wireless list only + hub = await setup_mikrotik_entry(hass) + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 is None + + with patch.object(mikrotik.hub.MikrotikData, "command", new=mock_command): + # test device_2 is added after connecting to wireless network + WIRELESS_DATA.append(DEVICE_2_WIRELESS) + + await hub.async_update() + await hass.async_block_till_done() + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 is not None + assert device_2.state == "home" + + # test state remains home if last_seen consider_home_interval + del WIRELESS_DATA[1] # device 2 is removed from wireless list + hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( + minutes=4 + ) + await hub.async_update() + await hass.async_block_till_done() + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state != "not_home" + + # test state changes to away if last_seen > consider_home_interval + hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( + minutes=5 + ) + await hub.async_update() + await hass.async_block_till_done() + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state == "not_home" + + +async def test_restoring_devices(hass): + """Test restoring existing device_tracker entities if not detected on startup.""" + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + registry = await entity_registry.async_get_registry(hass) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:01", + suggested_object_id="device_1", + config_entry=config_entry, + ) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:02", + suggested_object_id="device_2", + config_entry=config_entry, + ) + + await setup_mikrotik_entry(hass) + + # test device_2 which is not in wireless list is restored + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 is not None + assert device_2.state == "not_home" diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py new file mode 100644 index 00000000000..fc37c9113ae --- /dev/null +++ b/tests/components/mikrotik/test_hub.py @@ -0,0 +1,179 @@ +"""Test Mikrotik hub.""" +from asynctest import patch +import librouteros + +from homeassistant import config_entries +from homeassistant.components import mikrotik + +from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA + +from tests.common import MockConfigEntry + + +async def setup_mikrotik_entry(hass, **kwargs): + """Set up Mikrotik intergation successfully.""" + support_wireless = kwargs.get("support_wireless", True) + dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) + wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) + + def mock_command(self, cmd, params=None): + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return support_wireless + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return dhcp_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return wireless_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: + return ARP_DATA + return {} + + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + if "force_dhcp" in kwargs: + config_entry.options["force_dhcp"] = True + + if "arp_ping" in kwargs: + config_entry.options["arp_ping"] = True + + with patch("librouteros.connect"), patch.object( + mikrotik.hub.MikrotikData, "command", new=mock_command + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return hass.data[mikrotik.DOMAIN][config_entry.entry_id] + + +async def test_hub_setup_successful(hass): + """Successful setup of Mikrotik hub.""" + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + hub = await setup_mikrotik_entry(hass) + + assert hub.config_entry.data == { + mikrotik.CONF_NAME: "Mikrotik", + mikrotik.CONF_HOST: "0.0.0.0", + mikrotik.CONF_USERNAME: "user", + mikrotik.CONF_PASSWORD: "pass", + mikrotik.CONF_PORT: 8278, + mikrotik.CONF_VERIFY_SSL: False, + } + assert hub.config_entry.options == { + mikrotik.hub.CONF_FORCE_DHCP: False, + mikrotik.CONF_ARP_PING: False, + mikrotik.CONF_DETECTION_TIME: 300, + } + + assert hub.api.available is True + assert hub.signal_update == "mikrotik-update-0.0.0.0" + assert forward_entry_setup.mock_calls[0][1] == (hub.config_entry, "device_tracker") + + +async def test_hub_setup_failed(hass): + """Failed setup of Mikrotik hub.""" + + config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) + config_entry.add_to_hass(hass) + # error when connection fails + with patch( + "librouteros.connect", side_effect=librouteros.exceptions.ConnectionClosed + ): + + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + # error when username or password is invalid + config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + ) as forward_entry_setup, patch( + "librouteros.connect", + side_effect=librouteros.exceptions.TrapError("invalid user name or password"), + ): + + result = await hass.config_entries.async_setup(config_entry.entry_id) + + assert result is False + assert len(forward_entry_setup.mock_calls) == 0 + + +async def test_update_failed(hass): + """Test failing to connect during update.""" + + hub = await setup_mikrotik_entry(hass) + + with patch.object( + mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect + ): + await hub.async_update() + + assert hub.api.available is False + + +async def test_hub_not_support_wireless(hass): + """Test updating hub devices when hub doesn't support wireless interfaces.""" + + # test that the devices are constructed from dhcp data + + hub = await setup_mikrotik_entry(hass, support_wireless=False) + + assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] + assert hub.api.devices["00:00:00:00:00:01"]._wireless_params is None + assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] + assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None + + +async def test_hub_support_wireless(hass): + """Test updating hub devices when hub support wireless interfaces.""" + + # test that the device list is from wireless data list + + hub = await setup_mikrotik_entry(hass) + + assert hub.api.support_wireless is True + assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] + assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] + + # devices not in wireless list will not be added + assert "00:00:00:00:00:02" not in hub.api.devices + + +async def test_force_dhcp(hass): + """Test updating hub devices with forced dhcp method.""" + + # test that the devices are constructed from dhcp data + + hub = await setup_mikrotik_entry(hass, force_dhcp=True) + + assert hub.api.support_wireless is True + assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] + assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] + + # devices not in wireless list are added from dhcp + assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] + assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None + + +async def test_arp_ping(hass): + """Test arp ping devices to confirm they are connected.""" + + # test device show as home if arp ping returns value + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): + hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None + assert hub.api.devices["00:00:00:00:00:02"].last_seen is not None + + # test device show as away if arp ping times out + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): + hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None + # this device is not wireless so it will show as away + assert hub.api.devices["00:00:00:00:00:02"].last_seen is None diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py new file mode 100644 index 00000000000..bf2b19c735c --- /dev/null +++ b/tests/components/mikrotik/test_init.py @@ -0,0 +1,83 @@ +"""Test Mikrotik setup process.""" +from unittest.mock import Mock, patch + +from homeassistant.components import mikrotik +from homeassistant.setup import async_setup_component + +from . import MOCK_DATA + +from tests.common import MockConfigEntry, mock_coro + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a hub.""" + assert await async_setup_component(hass, mikrotik.DOMAIN, {}) is True + assert mikrotik.DOMAIN not in hass.data + + +async def test_successful_config_entry(hass): + """Test config entry successfull setup.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry.add_to_hass(hass) + mock_registry = Mock() + + with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( + "homeassistant.helpers.device_registry.async_get_registry", + return_value=mock_coro(mock_registry), + ): + mock_hub.return_value.async_setup.return_value = mock_coro(True) + mock_hub.return_value.serial_num = "12345678" + mock_hub.return_value.model = "RB750" + mock_hub.return_value.hostname = "mikrotik" + mock_hub.return_value.firmware = "3.65" + assert await mikrotik.async_setup_entry(hass, entry) is True + + assert len(mock_hub.mock_calls) == 2 + p_hass, p_entry = mock_hub.mock_calls[0][1] + + assert p_hass is hass + assert p_entry is entry + + assert len(mock_registry.mock_calls) == 1 + assert mock_registry.mock_calls[0][2] == { + "config_entry_id": entry.entry_id, + "connections": {("mikrotik", "12345678")}, + "manufacturer": mikrotik.ATTR_MANUFACTURER, + "model": "RB750", + "name": "mikrotik", + "sw_version": "3.65", + } + + +async def test_hub_fail_setup(hass): + """Test that a failed setup will not store the hub.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry.add_to_hass(hass) + + with patch.object(mikrotik, "MikrotikHub") as mock_hub: + mock_hub.return_value.async_setup.return_value = mock_coro(False) + assert await mikrotik.async_setup_entry(hass, entry) is False + + assert mikrotik.DOMAIN not in hass.data + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry.add_to_hass(hass) + + with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( + "homeassistant.helpers.device_registry.async_get_registry", + return_value=mock_coro(Mock()), + ): + mock_hub.return_value.async_setup.return_value = mock_coro(True) + mock_hub.return_value.serial_num = "12345678" + mock_hub.return_value.model = "RB750" + mock_hub.return_value.hostname = "mikrotik" + mock_hub.return_value.firmware = "3.65" + assert await mikrotik.async_setup_entry(hass, entry) is True + + assert len(mock_hub.return_value.mock_calls) == 1 + + assert await mikrotik.async_unload_entry(hass, entry) + assert entry.entry_id not in hass.data[mikrotik.DOMAIN] From d6d3feb54e0d86c39e7865acd09b2271d8186b1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 11:13:54 -0800 Subject: [PATCH 013/378] Guard Z-Wave light HS conversion on None (#31320) --- homeassistant/components/zwave/light.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 9c582eba89a..b32daf71f54 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -380,7 +380,9 @@ class ZwaveColorLight(ZwaveDimmer): # white LED must be off in order for color to work self._white = 0 - if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs: + if ( + ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs + ) and self._hs is not None: rgbw = "#" for colorval in color_util.color_hs_to_RGB(*self._hs): rgbw += format(colorval, "02x") From d5486f883d8063a38dd99e22047c32877c14b3ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 11:40:16 -0800 Subject: [PATCH 014/378] Fix wemo lights (#31323) --- homeassistant/components/wemo/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 8e43f47ef00..a615b3f5dfd 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -113,7 +113,7 @@ class WemoLight(Light): """Return the device info.""" return { "name": self.wemo.name, - "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, "model": self.wemo.model_name, "manufacturer": "Belkin", } From 3718b25bd9528188530f291f0810a1c7970abcdb Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 30 Jan 2020 21:14:46 +0100 Subject: [PATCH 015/378] Add opening and closing states to MQTT covers (#31259) * Added support for the opening and closing states to MQTT covers * Processed PR feedback on MQTT cover changes * Add missing MQTT abbreviation to fix failing tests * Fixed typo in MQTT abbreviations * Added mqtt set cover position optimistic test --- .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/cover.py | 53 +++++++++-- tests/components/mqtt/test_cover.py | 89 +++++++++++++++++++ 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 6f9b1720102..acbc2731846 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -132,9 +132,11 @@ ABBREVIATIONS = { "spds": "speeds", "src_type": "source_type", "stat_clsd": "state_closed", + "stat_closing": "state_closing", "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", + "stat_opening": "state_opening", "stat_locked": "state_locked", "stat_unlocked": "state_unlocked", "stat_t": "state_topic", diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index e6cfab90c26..4f2f29f94fb 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -25,7 +25,9 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -64,7 +66,9 @@ CONF_PAYLOAD_STOP = "payload_stop" CONF_POSITION_CLOSED = "position_closed" CONF_POSITION_OPEN = "position_open" CONF_STATE_CLOSED = "state_closed" +CONF_STATE_CLOSING = "state_closing" CONF_STATE_OPEN = "state_open" +CONF_STATE_OPENING = "state_opening" CONF_TILT_CLOSED_POSITION = "tilt_closed_value" CONF_TILT_INVERT_STATE = "tilt_invert_state" CONF_TILT_MAX = "tilt_max" @@ -131,7 +135,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, + vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, + vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION @@ -289,12 +295,20 @@ class MqttCover( payload = template.async_render_with_possible_json_value(payload) if payload == self._config[CONF_STATE_OPEN]: - self._state = False + self._state = STATE_OPEN + elif payload == self._config[CONF_STATE_OPENING]: + self._state = STATE_OPENING elif payload == self._config[CONF_STATE_CLOSED]: - self._state = True + self._state = STATE_CLOSED + elif payload == self._config[CONF_STATE_CLOSING]: + self._state = STATE_CLOSING else: - _LOGGER.warning("Payload is not True or False: %s", payload) + _LOGGER.warning( + "Payload is not supported (e.g. open, closed, opening, closing): %s", + payload, + ) return + self.async_write_ha_state() @callback @@ -309,7 +323,11 @@ class MqttCover( float(payload), COVER_PAYLOAD ) self._position = percentage_payload - self._state = percentage_payload == DEFAULT_POSITION_CLOSED + self._state = ( + STATE_CLOSED + if percentage_payload == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) else: _LOGGER.warning("Payload is not integer within range: %s", payload) return @@ -370,8 +388,21 @@ class MqttCover( @property def is_closed(self): - """Return if the cover is closed.""" - return self._state + """Return true if the cover is closed or None if the status is unknown.""" + if self._state is None: + return None + + return self._state == STATE_CLOSED + + @property + def is_opening(self): + """Return true if the cover is actively opening.""" + return self._state == STATE_OPENING + + @property + def is_closing(self): + """Return true if the cover is actively closing.""" + return self._state == STATE_CLOSING @property def current_cover_position(self): @@ -423,7 +454,7 @@ class MqttCover( ) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = False + self._state = STATE_OPEN if self._config.get(CONF_GET_POSITION_TOPIC): self._position = self.find_percentage_in_range( self._config[CONF_POSITION_OPEN], COVER_PAYLOAD @@ -444,7 +475,7 @@ class MqttCover( ) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = True + self._state = STATE_CLOSED if self._config.get(CONF_GET_POSITION_TOPIC): self._position = self.find_percentage_in_range( self._config[CONF_POSITION_CLOSED], COVER_PAYLOAD @@ -538,7 +569,11 @@ class MqttCover( self._config[CONF_RETAIN], ) if self._optimistic: - self._state = percentage_position == self._config[CONF_POSITION_CLOSED] + self._state = ( + STATE_CLOSED + if percentage_position == self._config[CONF_POSITION_CLOSED] + else STATE_OPEN + ) self._position = percentage_position self.async_write_ha_state() diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index b15518961a4..128c18de8df 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -19,7 +19,9 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -67,6 +69,93 @@ async def test_state_via_state_topic(hass, mqtt_mock): assert state.state == STATE_OPEN +async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_mock): + """Test the controlling opening and closing state via a custom payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "state_opening": "34", + "state_closing": "--43", + } + }, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "34") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPENING + + async_fire_mqtt_message(hass, "state-topic", "--43") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSING + + async_fire_mqtt_message(hass, "state-topic", STATE_CLOSED) + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + +async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): + """Test the state after setting the position using optimistic mode.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "position-topic", + "set_position_topic": "set-position-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "optimistic": True, + } + }, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 0}, + blocking=True, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 100}, + blocking=True, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_position_via_position_topic(hass, mqtt_mock): """Test the controlling state via topic.""" assert await async_setup_component( From 9ab6d08b97993904874ce77078fb6a676ca42957 Mon Sep 17 00:00:00 2001 From: endor <1937941+endor-force@users.noreply.github.com> Date: Thu, 30 Jan 2020 21:46:20 +0100 Subject: [PATCH 016/378] Bump pytrafikverket to 0.1.6.1 (#30697) * Bumped version for pytrafikverket * Updated version for pytrafikverket * Updated version for pytrafikverket --- homeassistant/components/trafikverket_train/manifest.json | 4 ++-- .../components/trafikverket_weatherstation/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 33f634e279f..1458b717fc6 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_train", "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": ["pytrafikverket==0.1.5.9"], + "requirements": ["pytrafikverket==0.1.6.1"], "dependencies": [], "codeowners": ["@endor-force"] -} +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 652cebf6730..3224df25c3f 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_weatherstation", "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": ["pytrafikverket==0.1.5.9"], + "requirements": ["pytrafikverket==0.1.6.1"], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index d8bf0d47386..ca276bf8035 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1675,7 +1675,7 @@ pytradfri[async]==6.4.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.1.5.9 +pytrafikverket==0.1.6.1 # homeassistant.components.ubee pyubee==0.8 From 73ea34e4171ece4fdb0391e4e975d0ca7f32a2ff Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 30 Jan 2020 16:13:45 -0500 Subject: [PATCH 017/378] Update media_player and add tests to qualify vizio integration for platinum quality score (#31187) * add media player test and update media player logic to qualify vizio for platinum quality score * add SCAN_INTERVAL and log message once when device goes from available to unavailable and vice versa * move test constants into one file to avoid defining dupes in each test file * move constant to shrink diff * move pytest fixtures to conftest.py * remove commented out code * move unload test to test_init * updates to tests and logging based on review * bypass notification service * add fixture so component setup makes it through config flow * split failure tests into two * fix setup and entity check for test_init and setup failures in test_media_player * make domain references consistent across test files * remove logging steps that were introduced to help debug * move common patches out to new fixture and use config entry everywhere appropriate * fix docstring * add test for update options to increase code coverage * add one more assert * refactor test_media_player to move boiler plate logic out of each test into _test_init function * final refactor of test_media_player to move repeat logic into separate function * update docstrings * refactor setup failure tests to move shared logic into private function * fix last new functions code to use variable instead of static config variable * remove trailing comma * test that volume_step gets properly passed to Vizio volume function * fix comment language * assert with unittest.mock.call in _test_service and use config_entries.async_setup instead of config_entries.async_add * replace config_entries.async_add with config_entries.async_setup everywhere * simplify if statement for argument assertion * fix logging based on style guide * remove accidentally committed changes * update scan interval to something more reasonable and add tests for availability changes * change filter function to list comprehension, simplify log messages, remove default entity id from logs since it is user configurable, fix docstrings --- .coveragerc | 3 - homeassistant/components/vizio/manifest.json | 3 +- .../components/vizio/media_player.py | 40 ++- tests/components/vizio/conftest.py | 102 ++++++ tests/components/vizio/const.py | 77 +++++ tests/components/vizio/test_config_flow.py | 139 ++------ tests/components/vizio/test_init.py | 43 +++ tests/components/vizio/test_media_player.py | 297 ++++++++++++++++++ 8 files changed, 559 insertions(+), 145 deletions(-) create mode 100644 tests/components/vizio/conftest.py create mode 100644 tests/components/vizio/const.py create mode 100644 tests/components/vizio/test_init.py create mode 100644 tests/components/vizio/test_media_player.py diff --git a/.coveragerc b/.coveragerc index 693959684f1..39a46d97616 100644 --- a/.coveragerc +++ b/.coveragerc @@ -782,9 +782,6 @@ omit = homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* homeassistant/components/vivotek/camera.py - homeassistant/components/vizio/__init__.py - homeassistant/components/vizio/const.py - homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index ea1162540cf..2f3a581e113 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -6,5 +6,6 @@ "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, - "zeroconf": ["_viziocast._tcp.local."] + "zeroconf": ["_viziocast._tcp.local."], + "quality_scale": "platinum" } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b2f529bce10..3ea70fe2acb 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,4 +1,5 @@ """Vizio SmartCast Device support.""" +from datetime import timedelta import logging from typing import Callable, List @@ -39,7 +40,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) - +SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -55,13 +56,13 @@ async def async_setup_entry( device_class = config_entry.data[CONF_DEVICE_CLASS] # If config entry options not set up, set them up, otherwise assign values managed in options + volume_step = config_entry.options.get( + CONF_VOLUME_STEP, config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP), + ) if not config_entry.options: - volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP) hass.config_entries.async_update_entry( config_entry, options={CONF_VOLUME_STEP: volume_step} ) - else: - volume_step = config_entry.options[CONF_VOLUME_STEP] device = VizioAsync( DEVICE_ID, @@ -74,18 +75,7 @@ async def async_setup_entry( ) if not await device.can_connect(): - fail_auth_msg = "" - if token: - fail_auth_msg = f"and auth token '{token}' are correct." - else: - fail_auth_msg = "is correct." - _LOGGER.warning( - "Failed to connect to Vizio device, please check if host '%s' " - "is valid and available. Also check if device class '%s' %s", - host, - device_class, - fail_auth_msg, - ) + _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady entity = VizioDevice(config_entry, device, name, volume_step, device_class) @@ -127,10 +117,18 @@ class VizioDevice(MediaPlayerDevice): is_on = await self._device.get_power_state(log_api_exception=False) if is_on is None: - self._available = False + if self._available: + _LOGGER.warning( + "Lost connection to %s", self._config_entry.data[CONF_HOST] + ) + self._available = False return - self._available = True + if not self._available: + _LOGGER.info( + "Restored connection to %s", self._config_entry.data[CONF_HOST] + ) + self._available = True if not is_on: self._state = STATE_OFF @@ -157,7 +155,7 @@ class VizioDevice(MediaPlayerDevice): async def _async_send_update_options_signal( hass: HomeAssistantType, config_entry: ConfigEntry ) -> None: - """Send update event when when Vizio config entry is updated.""" + """Send update event when Vizio config entry is updated.""" # Move this method to component level if another entity ever gets added for a single config entry. # See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121 async_dispatcher_send(hass, config_entry.entry_id, config_entry) @@ -276,7 +274,7 @@ class VizioDevice(MediaPlayerDevice): await self._device.input_switch(source) async def async_volume_up(self) -> None: - """Increasing volume of the device.""" + """Increase volume of the device.""" await self._device.vol_up(num=self._volume_step) if self._volume_level is not None: @@ -285,7 +283,7 @@ class VizioDevice(MediaPlayerDevice): ) async def async_volume_down(self) -> None: - """Decreasing volume of the device.""" + """Decrease volume of the device.""" await self._device.vol_down(num=self._volume_step) if self._volume_level is not None: diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py new file mode 100644 index 00000000000..fd78952bba4 --- /dev/null +++ b/tests/components/vizio/conftest.py @@ -0,0 +1,102 @@ +"""Configure py.test.""" +from asynctest import patch +import pytest +from pyvizio.const import DEVICE_CLASS_SPEAKER +from pyvizio.vizio import MAX_VOLUME + +from .const import CURRENT_INPUT, INPUT_LIST, UNIQUE_ID + + +class MockInput: + """Mock Vizio device input.""" + + def __init__(self, name): + """Initialize mock Vizio device input.""" + self.meta_name = name + self.name = name + + +def get_mock_inputs(input_list): + """Return list of MockInput.""" + return [MockInput(input) for input in input_list] + + +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +@pytest.fixture(name="vizio_connect") +def vizio_connect_fixture(): + """Mock valid vizio device and entry setup.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + return_value=True, + ), patch( + "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", + return_value=UNIQUE_ID, + ): + yield + + +@pytest.fixture(name="vizio_bypass_setup") +def vizio_bypass_setup_fixture(): + """Mock component setup.""" + with patch("homeassistant.components.vizio.async_setup_entry", return_value=True): + yield + + +@pytest.fixture(name="vizio_bypass_update") +def vizio_bypass_update_fixture(): + """Mock component update.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=True, + ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"): + yield + + +@pytest.fixture(name="vizio_guess_device_type") +def vizio_guess_device_type_fixture(): + """Mock vizio async_guess_device_type function.""" + with patch( + "homeassistant.components.vizio.config_flow.async_guess_device_type", + return_value="speaker", + ): + yield + + +@pytest.fixture(name="vizio_cant_connect") +def vizio_cant_connect_fixture(): + """Mock vizio device cant connect.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + return_value=False, + ): + yield + + +@pytest.fixture(name="vizio_update") +def vizio_update_fixture(): + """Mock valid updates to vizio device.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=True, + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_volume", + return_value=int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", + return_value=MockInput(CURRENT_INPUT), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_inputs", + return_value=get_mock_inputs(INPUT_LIST), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=True, + ): + yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py new file mode 100644 index 00000000000..dd6ecda55d0 --- /dev/null +++ b/tests/components/vizio/const.py @@ -0,0 +1,77 @@ +"""Constants for the Vizio integration tests.""" +import logging + +from homeassistant.components.media_player import ( + DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV, + DOMAIN as MP_DOMAIN, +) +from homeassistant.components.vizio.const import CONF_VOLUME_STEP +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, +) +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +NAME = "Vizio" +NAME2 = "Vizio2" +HOST = "192.168.1.1:9000" +HOST2 = "192.168.1.2:9000" +ACCESS_TOKEN = "deadbeef" +VOLUME_STEP = 2 +UNIQUE_ID = "testid" + +MOCK_USER_VALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, +} + +MOCK_OPTIONS = { + CONF_VOLUME_STEP: VOLUME_STEP, +} + +MOCK_IMPORT_VALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_VOLUME_STEP: VOLUME_STEP, +} + +MOCK_INVALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, +} + +MOCK_SPEAKER_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, +} + +VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." +ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" +ZEROCONF_HOST = HOST.split(":")[0] +ZEROCONF_PORT = HOST.split(":")[1] + +MOCK_ZEROCONF_SERVICE_INFO = { + CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE, + CONF_NAME: ZEROCONF_NAME, + CONF_HOST: ZEROCONF_HOST, + CONF_PORT: ZEROCONF_PORT, + "properties": {"name": "SB4031-D5"}, +} + +CURRENT_INPUT = "HDMI" +INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] + +ENTITY_ID = f"{MP_DOMAIN}.{slugify(NAME)}" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index c82c7a8de0f..9805d2def46 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -1,7 +1,4 @@ """Tests for Vizio config flow.""" -import logging - -from asynctest import patch import pytest import voluptuous as vol @@ -20,113 +17,26 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, - CONF_PORT, - CONF_TYPE, ) from homeassistant.helpers.typing import HomeAssistantType +from .const import ( + ACCESS_TOKEN, + HOST, + HOST2, + MOCK_IMPORT_VALID_TV_CONFIG, + MOCK_INVALID_TV_CONFIG, + MOCK_SPEAKER_CONFIG, + MOCK_USER_VALID_TV_CONFIG, + MOCK_ZEROCONF_SERVICE_INFO, + NAME, + NAME2, + UNIQUE_ID, + VOLUME_STEP, +) + from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - -NAME = "Vizio" -NAME2 = "Vizio2" -HOST = "192.168.1.1:9000" -HOST2 = "192.168.1.2:9000" -ACCESS_TOKEN = "deadbeef" -VOLUME_STEP = 2 -UNIQUE_ID = "testid" - -MOCK_USER_VALID_TV_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, - CONF_ACCESS_TOKEN: ACCESS_TOKEN, -} - -MOCK_IMPORT_VALID_TV_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, - CONF_ACCESS_TOKEN: ACCESS_TOKEN, - CONF_VOLUME_STEP: VOLUME_STEP, -} - -MOCK_INVALID_TV_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, -} - -MOCK_SPEAKER_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, -} - -VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." -ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" -ZEROCONF_HOST = HOST.split(":")[0] -ZEROCONF_PORT = HOST.split(":")[1] - -MOCK_ZEROCONF_ENTRY = { - CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE, - CONF_NAME: ZEROCONF_NAME, - CONF_HOST: ZEROCONF_HOST, - CONF_PORT: ZEROCONF_PORT, - "properties": {"name": "SB4031-D5"}, -} - - -@pytest.fixture(name="vizio_connect") -def vizio_connect_fixture(): - """Mock valid vizio device and entry setup.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", - return_value=True, - ), patch( - "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", - return_value=UNIQUE_ID, - ): - yield - - -@pytest.fixture(name="vizio_bypass_setup") -def vizio_bypass_setup_fixture(): - """Mock component setup.""" - with patch("homeassistant.components.vizio.async_setup_entry", return_value=True): - yield - - -@pytest.fixture(name="vizio_bypass_update") -def vizio_bypass_update_fixture(): - """Mock component update.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.can_connect", - return_value=True, - ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"): - yield - - -@pytest.fixture(name="vizio_guess_device_type") -def vizio_guess_device_type_fixture(): - """Mock vizio async_guess_device_type function.""" - with patch( - "homeassistant.components.vizio.config_flow.async_guess_device_type", - return_value="speaker", - ): - yield - - -@pytest.fixture(name="vizio_cant_connect") -def vizio_cant_connect_fixture(): - """Mock vizio device cant connect.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", - return_value=False, - ): - yield - async def test_user_flow_minimum_fields( hass: HomeAssistantType, @@ -142,12 +52,7 @@ async def test_user_flow_minimum_fields( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, - }, + result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -172,13 +77,7 @@ async def test_user_flow_all_fields( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, - CONF_ACCESS_TOKEN: ACCESS_TOKEN, - }, + result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -408,7 +307,7 @@ async def test_zeroconf_flow( vizio_guess_device_type: pytest.fixture, ) -> None: """Test zeroconf config flow.""" - discovery_info = MOCK_ZEROCONF_ENTRY.copy() + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -444,7 +343,7 @@ async def test_zeroconf_flow_already_configured( entry.add_to_hass(hass) # Try rediscovering same device - discovery_info = MOCK_ZEROCONF_ENTRY.copy() + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py new file mode 100644 index 00000000000..1be067e9570 --- /dev/null +++ b/tests/components/vizio/test_init.py @@ -0,0 +1,43 @@ +"""Tests for Vizio init.""" +import pytest + +from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN +from homeassistant.components.vizio.const import DOMAIN +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID + +from tests.common import MockConfigEntry + + +async def test_setup_component( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test component setup.""" + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: MOCK_USER_VALID_TV_CONFIG} + ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + + +async def test_load_and_unload( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test loading and unloading entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py new file mode 100644 index 00000000000..d87b86f8642 --- /dev/null +++ b/tests/components/vizio/test_media_player.py @@ -0,0 +1,297 @@ +"""Tests for Vizio config flow.""" +from datetime import timedelta +from unittest.mock import call + +from asynctest import patch +import pytest +from pyvizio.const import ( + DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, +) +from pyvizio.vizio import MAX_VOLUME + +from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV, + DOMAIN as MP_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_SELECT_SOURCE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, +) +from homeassistant.components.vizio.const import CONF_VOLUME_STEP, DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from .const import ( + CURRENT_INPUT, + ENTITY_ID, + INPUT_LIST, + MOCK_SPEAKER_CONFIG, + MOCK_USER_VALID_TV_CONFIG, + NAME, + UNIQUE_ID, + VOLUME_STEP, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def _test_setup( + hass: HomeAssistantType, ha_device_class: str, vizio_power_state: bool +) -> None: + """Test Vizio Device entity setup.""" + if vizio_power_state: + ha_power_state = STATE_ON + elif vizio_power_state is False: + ha_power_state = STATE_OFF + else: + ha_power_state = STATE_UNAVAILABLE + + if ha_device_class == DEVICE_CLASS_SPEAKER: + vizio_device_class = VIZIO_DEVICE_CLASS_SPEAKER + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID + ) + else: + vizio_device_class = VIZIO_DEVICE_CLASS_TV + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_volume", + return_value=int(MAX_VOLUME[vizio_device_class] / 2), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=vizio_power_state, + ): + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + attr = hass.states.get(ENTITY_ID).attributes + assert attr["friendly_name"] == NAME + assert attr["device_class"] == ha_device_class + + assert hass.states.get(ENTITY_ID).state == ha_power_state + if ha_power_state == STATE_ON: + assert attr["source_list"] == INPUT_LIST + assert attr["source"] == CURRENT_INPUT + assert ( + attr["volume_level"] + == float(int(MAX_VOLUME[vizio_device_class] / 2)) + / MAX_VOLUME[vizio_device_class] + ) + + +async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None: + """Test generic Vizio entity setup failure.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=False, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=config, unique_id=UNIQUE_ID) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + + +async def _test_service( + hass: HomeAssistantType, + vizio_func_name: str, + ha_service_name: str, + additional_service_data: dict = None, + *args, + **kwargs, +) -> None: + """Test generic Vizio media player entity service.""" + service_data = {ATTR_ENTITY_ID: ENTITY_ID} + if additional_service_data: + service_data.update(additional_service_data) + + with patch( + f"homeassistant.components.vizio.media_player.VizioAsync.{vizio_func_name}" + ) as service_call: + await hass.services.async_call( + MP_DOMAIN, ha_service_name, service_data=service_data, blocking=True, + ) + assert service_call.called + + if args or kwargs: + assert service_call.call_args == call(*args, **kwargs) + + +async def test_speaker_on( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio Speaker entity setup when on.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, True) + + +async def test_speaker_off( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio Speaker entity setup when off.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, False) + + +async def test_speaker_unavailable( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio Speaker entity setup when unavailable.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, None) + + +async def test_init_tv_on( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio TV entity setup when on.""" + await _test_setup(hass, DEVICE_CLASS_TV, True) + + +async def test_init_tv_off( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio TV entity setup when off.""" + await _test_setup(hass, DEVICE_CLASS_TV, False) + + +async def test_init_tv_unavailable( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio TV entity setup when unavailable.""" + await _test_setup(hass, DEVICE_CLASS_TV, None) + + +async def test_setup_failure_speaker( + hass: HomeAssistantType, vizio_connect: pytest.fixture +) -> None: + """Test speaker entity setup failure.""" + await _test_setup_failure(hass, MOCK_SPEAKER_CONFIG) + + +async def test_setup_failure_tv( + hass: HomeAssistantType, vizio_connect: pytest.fixture +) -> None: + """Test TV entity setup failure.""" + await _test_setup_failure(hass, MOCK_USER_VALID_TV_CONFIG) + + +async def test_services( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test all Vizio media player entity services.""" + await _test_setup(hass, DEVICE_CLASS_TV, True) + + await _test_service(hass, "pow_on", SERVICE_TURN_ON) + await _test_service(hass, "pow_off", SERVICE_TURN_OFF) + await _test_service( + hass, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + ) + await _test_service( + hass, "mute_off", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False} + ) + await _test_service( + hass, "input_switch", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB" + ) + await _test_service(hass, "vol_up", SERVICE_VOLUME_UP) + await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN) + await _test_service( + hass, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + ) + await _test_service( + hass, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + ) + await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK) + await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK) + + +async def test_options_update( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test when config entry update event fires.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, True) + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.options + new_options = config_entry.options.copy() + updated_options = {CONF_VOLUME_STEP: VOLUME_STEP} + new_options.update(updated_options) + hass.config_entries.async_update_entry( + entry=config_entry, options=new_options, + ) + assert config_entry.options == updated_options + await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, num=VOLUME_STEP) + + +async def _test_update_availability_switch( + hass: HomeAssistantType, + initial_power_state: bool, + final_power_state: bool, + caplog: pytest.fixture, +) -> None: + now = dt_util.utcnow() + future_interval = timedelta(minutes=1) + + # Setup device as if time is right now + with patch("homeassistant.util.dt.utcnow", return_value=now): + await _test_setup(hass, DEVICE_CLASS_SPEAKER, initial_power_state) + + # Clear captured logs so that only availability state changes are captured for + # future assertion + caplog.clear() + + # Fast forward time to future twice to trigger update and assert vizio log message + for i in range(1, 3): + future = now + (future_interval * i) + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=final_power_state, + ), patch("homeassistant.util.dt.utcnow", return_value=future), patch( + "homeassistant.util.utcnow", return_value=future + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + if final_power_state is None: + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE + else: + assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE + + # Ensure connection status messages from vizio.media_player appear exactly once + # (on availability state change) + vizio_log_list = [ + log + for log in caplog.records + if log.name == "homeassistant.components.vizio.media_player" + ] + assert len(vizio_log_list) == 1 + + +async def test_update_unavailable_to_available( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device becomes available after being unavailable.""" + await _test_update_availability_switch(hass, None, True, caplog) + + +async def test_update_available_to_unavailable( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device becomes unavailable after being available.""" + await _test_update_availability_switch(hass, True, None, caplog) From cd1aa464047d05b8a8621b717cf2729890737070 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 30 Jan 2020 22:14:43 +0100 Subject: [PATCH 018/378] Register on HA stop event to gracefully shutdown HomematicIP Cloud connections (#31289) * Register on HA stop event to gracefully shutdown HomematicIP Cloud connections * fixes after review * Fix lint * switch to unload_entry * Save listener * Switch back to hap.async_reset() --- .../components/homematicip_cloud/__init__.py | 13 ++++++++++++- homeassistant/components/homematicip_cloud/hap.py | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 46bf300753f..63d1f82071b 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids @@ -348,6 +348,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if not await hap.async_setup(): return False + async def async_reset_hap_connection(): + """Reset hmip hap connection.""" + await hap.async_reset() + _LOGGER.debug("Reset connection to access point id %s", entry.unique_id) + + # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection + hap.reset_connection_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_reset_hap_connection() + ) + # Register hap as device in registry. device_registry = await dr.async_get_registry(hass) home = hap.home @@ -367,4 +377,5 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.unique_id) + hap.reset_connection_listener() return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 3c97cc1af9f..8a4b7ed5fae 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -81,6 +81,7 @@ class HomematicipHAP: self._tries = 0 self._accesspoint_connected = True self.hmip_device_by_entity_id = {} + self.reset_connection_listener = None async def async_setup(self, tries: int = 0) -> bool: """Initialize connection.""" From 56657fa859369b30b3fe6930ecd518cc079c5cc5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 30 Jan 2020 22:20:30 +0100 Subject: [PATCH 019/378] Axis - config flow use new helper functions (#31286) * Make use of new config flow helpers Simplify Axis entry config to work with config flow helpers * Keep old device data for rollback purposes --- homeassistant/components/axis/__init__.py | 33 ++++++- homeassistant/components/axis/camera.py | 17 ++-- homeassistant/components/axis/config_flow.py | 92 +++++++++----------- homeassistant/components/axis/device.py | 30 ++++--- homeassistant/components/axis/strings.json | 5 +- tests/components/axis/test_config_flow.py | 63 ++++++++------ tests/components/axis/test_device.py | 27 +++--- tests/components/axis/test_init.py | 46 +++++++++- 8 files changed, 183 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5c928aa9f31..5294e30ed6f 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -1,15 +1,23 @@ """Support for Axis devices.""" +import logging + from homeassistant.const import ( CONF_DEVICE, + CONF_HOST, CONF_MAC, + CONF_PASSWORD, + CONF_PORT, CONF_TRIGGER_TIME, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN from .device import AxisNetworkDevice, get_device +LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Old way to set up Axis devices.""" @@ -35,7 +43,7 @@ async def async_setup_entry(hass, config_entry): config_entry, unique_id=device.api.vapix.params.system_serialnumber ) - hass.data[DOMAIN][device.serial] = device + hass.data[DOMAIN][config_entry.unique_id] = device await device.async_update_device_registry() @@ -52,7 +60,13 @@ async def async_unload_entry(hass, config_entry): async def async_populate_options(hass, config_entry): """Populate default options for device.""" - device = await get_device(hass, config_entry.data[CONF_DEVICE]) + device = await get_device( + hass, + host=config_entry.data[CONF_HOST], + port=config_entry.data[CONF_PORT], + username=config_entry.data[CONF_USERNAME], + password=config_entry.data[CONF_PASSWORD], + ) supported_formats = device.vapix.params.image_format camera = bool(supported_formats) @@ -64,3 +78,18 @@ async def async_populate_options(hass, config_entry): } hass.config_entries.async_update_entry(config_entry, options=options) + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + LOGGER.debug("Migrating from version %s", config_entry.version) + + # Flatten configuration but keep old data if user rollbacks HASS + if config_entry.version == 1: + config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} + + config_entry.version = 2 + + LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 6b82c938a99..51d6b6805cc 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -9,7 +9,6 @@ from homeassistant.components.mjpeg.camera import ( ) from homeassistant.const import ( CONF_AUTHENTICATION, - CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -35,15 +34,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = { CONF_NAME: config_entry.data[CONF_NAME], - CONF_USERNAME: config_entry.data[CONF_DEVICE][CONF_USERNAME], - CONF_PASSWORD: config_entry.data[CONF_DEVICE][CONF_PASSWORD], + CONF_USERNAME: config_entry.data[CONF_USERNAME], + CONF_PASSWORD: config_entry.data[CONF_PASSWORD], CONF_MJPEG_URL: AXIS_VIDEO.format( - config_entry.data[CONF_DEVICE][CONF_HOST], - config_entry.data[CONF_DEVICE][CONF_PORT], + config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], ), CONF_STILL_IMAGE_URL: AXIS_IMAGE.format( - config_entry.data[CONF_DEVICE][CONF_HOST], - config_entry.data[CONF_DEVICE][CONF_PORT], + config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], ), CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, } @@ -76,14 +73,14 @@ class AxisCamera(AxisEntityBase, MjpegCamera): async def stream_source(self): """Return the stream source.""" return AXIS_STREAM.format( - self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME], - self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD], + self.device.config_entry.data[CONF_USERNAME], + self.device.config_entry.data[CONF_PASSWORD], self.device.host, ) def _new_address(self): """Set new device address for video stream.""" - port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT] + port = self.device.config_entry.data[CONF_PORT] self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port) self._still_image_url = AXIS_IMAGE.format(self.device.host, port) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 88c1cab98c1..29658c19c5b 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -4,7 +4,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, @@ -33,16 +32,12 @@ DEFAULT_PORT = 80 class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Axis config flow.""" self.device_config = {} - self.model = None - self.name = None - self.serial_number = None - self.discovery_schema = {} self.import_schema = {} @@ -55,24 +50,32 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: + device = await get_device( + self.hass, + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + serial_number = device.vapix.params.system_serialnumber + await self.async_set_unique_id(serial_number) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) + self.device_config = { CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_MAC: serial_number, + CONF_MODEL: device.vapix.params.prodnbr, } - device = await get_device(self.hass, self.device_config) - - self.serial_number = device.vapix.params.system_serialnumber - config_entry = await self.async_set_unique_id(self.serial_number) - if config_entry: - return self._update_entry( - config_entry, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - ) - - self.model = device.vapix.params.prodnbr return await self._create_entry() @@ -101,41 +104,23 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): Generate a name to be used as a prefix for device entities. """ + model = self.device_config[CONF_MODEL] same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.data[CONF_MODEL] == self.model + if entry.data[CONF_MODEL] == model ] - self.name = f"{self.model}" + name = model for idx in range(len(same_model) + 1): - self.name = f"{self.model} {idx}" - if self.name not in same_model: + name = f"{model} {idx}" + if name not in same_model: break - data = { - CONF_DEVICE: self.device_config, - CONF_NAME: self.name, - CONF_MAC: self.serial_number, - CONF_MODEL: self.model, - } + self.device_config[CONF_NAME] = name - title = f"{self.model} - {self.serial_number}" - return self.async_create_entry(title=title, data=data) - - def _update_entry(self, entry, host, port): - """Update existing entry.""" - if ( - entry.data[CONF_DEVICE][CONF_HOST] == host - and entry.data[CONF_DEVICE][CONF_PORT] == port - ): - return self.async_abort(reason="already_configured") - - entry.data[CONF_DEVICE][CONF_HOST] = host - entry.data[CONF_DEVICE][CONF_PORT] = port - - self.hass.config_entries.async_update_entry(entry) - return self.async_abort(reason="updated_configuration") + title = f"{model} - {self.device_config[CONF_MAC]}" + return self.async_create_entry(title=title, data=self.device_config) async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered Axis device.""" @@ -147,18 +132,19 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - config_entry = await self.async_set_unique_id(serial_number) - if config_entry: - return self._update_entry( - config_entry, - host=discovery_info[CONF_HOST], - port=discovery_info[CONF_PORT], - ) + await self.async_set_unique_id(serial_number) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: discovery_info[CONF_HOST], + CONF_PORT: discovery_info[CONF_PORT], + } + ) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - "name": discovery_info["hostname"][:-7], - "host": discovery_info[CONF_HOST], + CONF_NAME: discovery_info["hostname"][:-7], + CONF_HOST: discovery_info[CONF_HOST], } self.discovery_schema = { diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 85ad59268df..a204136e018 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -7,9 +7,7 @@ import axis from axis.streammanager import SIGNAL_PLAYING from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, - CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -42,7 +40,7 @@ class AxisNetworkDevice: @property def host(self): """Return the host of this device.""" - return self.config_entry.data[CONF_DEVICE][CONF_HOST] + return self.config_entry.data[CONF_HOST] @property def model(self): @@ -75,7 +73,13 @@ class AxisNetworkDevice: async def async_setup(self): """Set up the device.""" try: - self.api = await get_device(self.hass, self.config_entry.data[CONF_DEVICE]) + self.api = await get_device( + self.hass, + host=self.config_entry.data[CONF_HOST], + port=self.config_entry.data[CONF_PORT], + username=self.config_entry.data[CONF_USERNAME], + password=self.config_entry.data[CONF_PASSWORD], + ) except CannotConnect: raise ConfigEntryNotReady @@ -126,7 +130,7 @@ class AxisNetworkDevice: This is a static method because a class method (bound method), can not be used with weak references. """ - device = hass.data[DOMAIN][entry.data[CONF_MAC]] + device = hass.data[DOMAIN][entry.unique_id] device.api.config.host = device.host async_dispatcher_send(hass, device.event_new_address) @@ -197,15 +201,15 @@ class AxisNetworkDevice: return True -async def get_device(hass, config): +async def get_device(hass, host, port, username, password): """Create a Axis device.""" device = axis.AxisDevice( loop=hass.loop, - host=config[CONF_HOST], - username=config[CONF_USERNAME], - password=config[CONF_PASSWORD], - port=config[CONF_PORT], + host=host, + port=port, + username=username, + password=password, web_proto="http", ) @@ -224,13 +228,11 @@ async def get_device(hass, config): return device except axis.Unauthorized: - LOGGER.warning( - "Connected to device at %s but not registered.", config[CONF_HOST] - ) + LOGGER.warning("Connected to device at %s but not registered.", host) raise AuthenticationRequired except (asyncio.TimeoutError, axis.RequestError): - LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) + LOGGER.error("Error connecting to the Axis device at %s", host) raise CannotConnect except axis.AxisException: diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 7facd7060ad..b50a5c546b8 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -23,8 +23,7 @@ "already_configured": "Device is already configured", "bad_config_file": "Bad data from config file", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device", - "updated_configuration": "Updated device configuration with new host address" + "not_axis_device": "Discovered device not an Axis device" } } -} +} \ No newline at end of file diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 809c71c5cb1..2e4c3e9f8be 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components import axis from homeassistant.components.axis import config_flow -from .test_device import MAC, setup_axis_integration +from .test_device import MAC, MODEL, NAME, setup_axis_integration from tests.common import MockConfigEntry, mock_coro @@ -54,12 +54,10 @@ async def test_flow_manual_configuration(hass): assert result["type"] == "create_entry" assert result["title"] == f"prodnbr - {MAC}" assert result["data"] == { - axis.CONF_DEVICE: { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, - }, + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_USERNAME: "user", + config_flow.CONF_PASSWORD: "pass", + config_flow.CONF_PORT: 80, config_flow.CONF_MAC: MAC, config_flow.CONF_MODEL: "prodnbr", config_flow.CONF_NAME: "prodnbr 0", @@ -95,11 +93,8 @@ async def test_manual_configuration_update_configuration(hass): ) assert result["type"] == "abort" - assert result["reason"] == "updated_configuration" - assert ( - device.config_entry.data[config_flow.CONF_DEVICE][config_flow.CONF_HOST] - == "2.3.4.5" - ) + assert result["reason"] == "already_configured" + assert device.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5" async def test_flow_fails_already_configured(hass): @@ -223,12 +218,10 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["type"] == "create_entry" assert result["title"] == f"prodnbr - {MAC}" assert result["data"] == { - axis.CONF_DEVICE: { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, - }, + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_USERNAME: "user", + config_flow.CONF_PASSWORD: "pass", + config_flow.CONF_PORT: 80, config_flow.CONF_MAC: MAC, config_flow.CONF_MODEL: "prodnbr", config_flow.CONF_NAME: "prodnbr 2", @@ -271,12 +264,10 @@ async def test_zeroconf_flow(hass): assert result["type"] == "create_entry" assert result["title"] == f"prodnbr - {MAC}" assert result["data"] == { - axis.CONF_DEVICE: { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, - }, + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_USERNAME: "user", + config_flow.CONF_PASSWORD: "pass", + config_flow.CONF_PORT: 80, config_flow.CONF_MAC: MAC, config_flow.CONF_MODEL: "prodnbr", config_flow.CONF_NAME: "prodnbr 0", @@ -310,6 +301,15 @@ async def test_zeroconf_flow_updated_configuration(hass): """Test that zeroconf update configuration with new parameters.""" device = await setup_axis_integration(hass) assert device.host == "1.2.3.4" + assert device.config_entry.data == { + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 80, + config_flow.CONF_USERNAME: "username", + config_flow.CONF_PASSWORD: "password", + config_flow.CONF_MAC: MAC, + config_flow.CONF_MODEL: MODEL, + config_flow.CONF_NAME: NAME, + } result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -323,11 +323,16 @@ async def test_zeroconf_flow_updated_configuration(hass): ) assert result["type"] == "abort" - assert result["reason"] == "updated_configuration" - assert device.host == "2.3.4.5" - assert ( - device.config_entry.data[config_flow.CONF_DEVICE][config_flow.CONF_PORT] == 8080 - ) + assert result["reason"] == "already_configured" + assert device.config_entry.data == { + config_flow.CONF_HOST: "2.3.4.5", + config_flow.CONF_PORT: 8080, + config_flow.CONF_USERNAME: "username", + config_flow.CONF_PASSWORD: "password", + config_flow.CONF_MAC: MAC, + config_flow.CONF_MODEL: MODEL, + config_flow.CONF_NAME: NAME, + } async def test_zeroconf_flow_ignore_non_axis_device(hass): diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index b175d22cfb4..3d2ed432c1c 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -14,18 +14,14 @@ MAC = "00408C12345" MODEL = "model" NAME = "name" -DEVICE_DATA = { - axis.device.CONF_HOST: "1.2.3.4", - axis.device.CONF_USERNAME: "username", - axis.device.CONF_PASSWORD: "password", - axis.device.CONF_PORT: 80, -} - -ENTRY_OPTIONS = {axis.device.CONF_CAMERA: True, axis.device.CONF_EVENTS: True} +ENTRY_OPTIONS = {axis.CONF_CAMERA: True, axis.CONF_EVENTS: True} ENTRY_CONFIG = { - axis.device.CONF_DEVICE: DEVICE_DATA, - axis.device.CONF_MAC: MAC, + axis.CONF_HOST: "1.2.3.4", + axis.CONF_USERNAME: "username", + axis.CONF_PASSWORD: "password", + axis.CONF_PORT: 80, + axis.CONF_MAC: MAC, axis.device.CONF_MODEL: MODEL, axis.device.CONF_NAME: NAME, } @@ -76,6 +72,7 @@ async def setup_axis_integration( connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, options=deepcopy(options), entry_id="1", + version=2, ) config_entry.add_to_hass(hass) @@ -116,10 +113,10 @@ async def test_device_setup(hass): assert forward_entry_setup.mock_calls[1][1] == (entry, "binary_sensor") assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") - assert device.host == DEVICE_DATA[axis.device.CONF_HOST] + assert device.host == ENTRY_CONFIG[axis.CONF_HOST] assert device.model == ENTRY_CONFIG[axis.device.CONF_MODEL] assert device.name == ENTRY_CONFIG[axis.device.CONF_NAME] - assert device.serial == ENTRY_CONFIG[axis.device.CONF_MAC] + assert device.serial == ENTRY_CONFIG[axis.CONF_MAC] async def test_update_address(hass): @@ -204,7 +201,7 @@ async def test_get_device_fails(hass): with patch( "axis.param_cgi.Params.update_brand", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, DEVICE_DATA) + await axis.device.get_device(hass, host="", port="", username="", password="") async def test_get_device_device_unavailable(hass): @@ -212,7 +209,7 @@ async def test_get_device_device_unavailable(hass): with patch( "axis.param_cgi.Params.update_brand", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_device(hass, DEVICE_DATA) + await axis.device.get_device(hass, host="", port="", username="", password="") async def test_get_device_unknown_error(hass): @@ -220,4 +217,4 @@ async def test_get_device_unknown_error(hass): with patch( "axis.param_cgi.Params.update_brand", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, DEVICE_DATA) + await axis.device.get_device(hass, host="", port="", username="", password="") diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 748bb539369..643ddf58ac0 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components import axis from homeassistant.setup import async_setup_component -from .test_device import MAC, setup_axis_integration +from .test_device import ENTRY_CONFIG, MAC, setup_axis_integration from tests.common import MockConfigEntry, mock_coro @@ -16,7 +16,7 @@ async def test_setup_device_already_configured(hass): assert await async_setup_component( hass, axis.DOMAIN, - {axis.DOMAIN: {"device_name": {axis.config_flow.CONF_HOST: "1.2.3.4"}}}, + {axis.DOMAIN: {"device_name": {axis.CONF_HOST: "1.2.3.4"}}}, ) assert not mock_config_entries.flow.mock_calls @@ -38,7 +38,7 @@ async def test_setup_entry(hass): async def test_setup_entry_fails(hass): """Test successful setup of entry.""" entry = MockConfigEntry( - domain=axis.DOMAIN, data={axis.device.CONF_MAC: "0123"}, options=True + domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, options=True ) mock_device = Mock() @@ -63,7 +63,7 @@ async def test_unload_entry(hass): async def test_populate_options(hass): """Test successful populate options.""" - entry = MockConfigEntry(domain=axis.DOMAIN, data={"device": {}}) + entry = MockConfigEntry(domain=axis.DOMAIN, data=ENTRY_CONFIG) entry.add_to_hass(hass) with patch.object(axis, "get_device", return_value=mock_coro(Mock())): @@ -75,3 +75,41 @@ async def test_populate_options(hass): axis.CONF_EVENTS: True, axis.CONF_TRIGGER_TIME: axis.DEFAULT_TRIGGER_TIME, } + + +async def test_migrate_entry(hass): + """Test successful migration of entry data.""" + legacy_config = { + axis.CONF_DEVICE: { + axis.CONF_HOST: "1.2.3.4", + axis.CONF_USERNAME: "username", + axis.CONF_PASSWORD: "password", + axis.CONF_PORT: 80, + }, + axis.CONF_MAC: "mac", + axis.device.CONF_MODEL: "model", + axis.device.CONF_NAME: "name", + } + entry = MockConfigEntry(domain=axis.DOMAIN, data=legacy_config) + + assert entry.data == legacy_config + assert entry.version == 1 + + await axis.async_migrate_entry(hass, entry) + + assert entry.data == { + axis.CONF_DEVICE: { + axis.CONF_HOST: "1.2.3.4", + axis.CONF_USERNAME: "username", + axis.CONF_PASSWORD: "password", + axis.CONF_PORT: 80, + }, + axis.CONF_HOST: "1.2.3.4", + axis.CONF_USERNAME: "username", + axis.CONF_PASSWORD: "password", + axis.CONF_PORT: 80, + axis.CONF_MAC: "mac", + axis.device.CONF_MODEL: "model", + axis.device.CONF_NAME: "name", + } + assert entry.version == 2 From a8374cf423cd9b5eab2631bb0b335cdc33356a64 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 30 Jan 2020 23:06:43 +0100 Subject: [PATCH 020/378] UniFi - Try to discover local controller (#31326) * Its working * Use "unifi" as default host if a controller can be found * Fix tests * Make a fixture of patching the discovery function --- homeassistant/components/unifi/config_flow.py | 16 +++++++++++++++- tests/components/unifi/conftest.py | 13 +++++++++++++ tests/components/unifi/test_config_flow.py | 10 +++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/components/unifi/conftest.py diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 52ecab08856..9dbacc7916d 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,4 +1,6 @@ """Config flow for UniFi.""" +import socket + import voluptuous as vol from homeassistant import config_entries @@ -104,11 +106,15 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="unknown") + host = "" + if await async_discover_unifi(self.hass): + host = "unifi" + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST, default=host): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, @@ -235,3 +241,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def _update_options(self): """Update config entry options.""" return self.async_create_entry(title="", data=self.options) + + +async def async_discover_unifi(hass): + """Discover UniFi address.""" + try: + return await hass.async_add_executor_job(socket.gethostbyname, "unifi") + except socket.gaierror: + return None diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py new file mode 100644 index 00000000000..189b80c1932 --- /dev/null +++ b/tests/components/unifi/conftest.py @@ -0,0 +1,13 @@ +"""Fixtures for UniFi methods.""" +from asynctest import patch +import pytest + + +@pytest.fixture(autouse=True) +def mock_discovery(): + """No real network traffic allowed.""" + with patch( + "homeassistant.components.unifi.config_flow.async_discover_unifi", + return_value=None, + ) as mock: + yield mock diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index cc8896d55ce..c2c89d2b9c0 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -16,14 +16,22 @@ from homeassistant.const import ( from tests.common import MockConfigEntry -async def test_flow_works(hass, aioclient_mock): +async def test_flow_works(hass, aioclient_mock, mock_discovery): """Test config flow.""" + mock_discovery.return_value = "1" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" assert result["step_id"] == "user" + assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { + CONF_HOST: "unifi", + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_PORT: 8443, + CONF_VERIFY_SSL: False, + } aioclient_mock.post( "https://1.2.3.4:1234/api/login", From 611127a6bcf61f9819280b88e3773b29d1315a15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 31 Jan 2020 00:04:59 +0100 Subject: [PATCH 021/378] Bump pytest to 5.3.5 (#31327) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8d1a8ba287e..bd2875f7115 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,6 +15,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.3.4 +pytest==5.3.5 requests_mock==1.7.0 responses==0.10.6 From 74413e07d0a01ec4a0efd76a845760dc82a6713b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 31 Jan 2020 00:31:57 +0000 Subject: [PATCH 022/378] [ci skip] Translation update --- .../.translations/zh-Hant.json | 20 +- .../binary_sensor/.translations/zh-Hant.json | 172 +++++++++--------- .../components/brother/.translations/ca.json | 8 + .../climate/.translations/zh-Hant.json | 12 +- .../cover/.translations/zh-Hant.json | 24 +-- .../components/deconz/.translations/ca.json | 8 +- .../device_tracker/.translations/zh-Hant.json | 4 +- .../components/fan/.translations/zh-Hant.json | 12 +- .../garmin_connect/.translations/ca.json | 24 +++ .../components/gios/.translations/ca.json | 3 + .../light/.translations/zh-Hant.json | 14 +- .../components/linky/.translations/ca.json | 1 + .../lock/.translations/zh-Hant.json | 14 +- .../media_player/.translations/zh-Hant.json | 10 +- .../components/mikrotik/.translations/ca.json | 33 ++++ .../components/mikrotik/.translations/da.json | 37 ++++ .../components/mikrotik/.translations/en.json | 44 ++--- .../components/mikrotik/.translations/ru.json | 37 ++++ .../components/netatmo/.translations/ca.json | 3 +- .../components/point/.translations/ca.json | 2 +- .../components/ring/.translations/ca.json | 4 + .../samsungtv/.translations/ca.json | 7 + .../sensor/.translations/zh-Hant.json | 36 ++-- .../components/soma/.translations/ca.json | 2 +- .../components/somfy/.translations/ca.json | 4 +- .../components/spotify/.translations/ca.json | 18 ++ .../switch/.translations/zh-Hant.json | 18 +- .../tellduslive/.translations/ca.json | 2 +- .../vacuum/.translations/zh-Hant.json | 12 +- .../components/vizio/.translations/ca.json | 4 + .../components/withings/.translations/ca.json | 2 + 31 files changed, 388 insertions(+), 203 deletions(-) create mode 100644 homeassistant/components/garmin_connect/.translations/ca.json create mode 100644 homeassistant/components/mikrotik/.translations/ca.json create mode 100644 homeassistant/components/mikrotik/.translations/da.json create mode 100644 homeassistant/components/mikrotik/.translations/ru.json create mode 100644 homeassistant/components/spotify/.translations/ca.json diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json index 72c0b65436d..94729865c6f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -1,18 +1,18 @@ { "device_automation": { "action_type": { - "arm_away": "\u8a2d\u5b9a {entity_name} \u5916\u51fa\u6a21\u5f0f", - "arm_home": "\u8a2d\u5b9a {entity_name} \u8fd4\u5bb6\u6a21\u5f0f", - "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", - "disarm": "\u89e3\u9664 {entity_name}", - "trigger": "\u89f8\u767c {entity_name}" + "arm_away": "\u8a2d\u5b9a{entity_name}\u5916\u51fa\u6a21\u5f0f", + "arm_home": "\u8a2d\u5b9a{entity_name}\u8fd4\u5bb6\u6a21\u5f0f", + "arm_night": "\u8a2d\u5b9a{entity_name}\u591c\u9593\u6a21\u5f0f", + "disarm": "\u89e3\u9664{entity_name}", + "trigger": "\u89f8\u767c{entity_name}" }, "trigger_type": { - "armed_away": "{entity_name} \u8a2d\u5b9a\u5916\u51fa", - "armed_home": "{entity_name} \u8a2d\u5b9a\u5728\u5bb6", - "armed_night": "{entity_name} \u8a2d\u5b9a\u591c\u9593", - "disarmed": "{entity_name} \u5df2\u89e3\u9664", - "triggered": "{entity_name} \u5df2\u89f8\u767c" + "armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", + "armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", + "armed_night": "{entity_name}\u8a2d\u5b9a\u591c\u9593", + "disarmed": "{entity_name}\u5df2\u89e3\u9664", + "triggered": "{entity_name}\u5df2\u89f8\u767c" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json index 046b999cb8c..712c0fbd7c1 100644 --- a/homeassistant/components/binary_sensor/.translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -1,94 +1,94 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \u96fb\u91cf\u904e\u4f4e", - "is_cold": "{entity_name} \u51b7", - "is_connected": "{entity_name} \u5df2\u9023\u7dda", - "is_gas": "{entity_name} \u5075\u6e2c\u5230\u6c23\u9ad4", - "is_hot": "{entity_name} \u71b1", - "is_light": "{entity_name} \u5075\u6e2c\u5230\u5149\u7dda\u4e2d", - "is_locked": "{entity_name} \u5df2\u4e0a\u9396", - "is_moist": "{entity_name} \u6f6e\u6fd5", - "is_motion": "{entity_name} \u5075\u6e2c\u5230\u52d5\u4f5c\u4e2d", - "is_moving": "{entity_name} \u79fb\u52d5\u4e2d", - "is_no_gas": "{entity_name} \u672a\u5075\u6e2c\u5230\u6c23\u9ad4", - "is_no_light": "{entity_name} \u672a\u5075\u6e2c\u5230\u5149\u7dda", - "is_no_motion": "{entity_name} \u672a\u5075\u6e2c\u5230\u52d5\u4f5c", - "is_no_problem": "{entity_name} \u672a\u5075\u6e2c\u5230\u554f\u984c", - "is_no_smoke": "{entity_name} \u672a\u5075\u6e2c\u5230\u7159\u9727", - "is_no_sound": "{entity_name} \u672a\u5075\u6e2c\u5230\u8072\u97f3", - "is_no_vibration": "{entity_name} \u672a\u5075\u6e2c\u5230\u9707\u52d5", - "is_not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", - "is_not_cold": "{entity_name} \u4e0d\u51b7", - "is_not_connected": "{entity_name} \u65b7\u7dda", - "is_not_hot": "{entity_name} \u4e0d\u71b1", - "is_not_locked": "{entity_name} \u89e3\u9396", - "is_not_moist": "{entity_name} \u4e7e\u71e5", - "is_not_moving": "{entity_name} \u672a\u5728\u79fb\u52d5", - "is_not_occupied": "{entity_name} \u672a\u6709\u4eba", - "is_not_open": "{entity_name} \u95dc\u9589", - "is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165", - "is_not_powered": "{entity_name} \u672a\u901a\u96fb", - "is_not_present": "{entity_name} \u672a\u51fa\u73fe", - "is_not_unsafe": "{entity_name} \u5b89\u5168", - "is_occupied": "{entity_name} \u6709\u4eba", - "is_off": "{entity_name} \u95dc\u9589", - "is_on": "{entity_name} \u958b\u555f", - "is_open": "{entity_name} \u958b\u555f", - "is_plugged_in": "{entity_name} \u63d2\u5165", - "is_powered": "{entity_name} \u901a\u96fb", - "is_present": "{entity_name} \u51fa\u73fe", - "is_problem": "{entity_name} \u6b63\u5075\u6e2c\u5230\u554f\u984c", - "is_smoke": "{entity_name} \u6b63\u5075\u6e2c\u5230\u7159\u9727", - "is_sound": "{entity_name} \u6b63\u5075\u6e2c\u5230\u8072\u97f3", - "is_unsafe": "{entity_name} \u4e0d\u5b89\u5168", - "is_vibration": "{entity_name} \u6b63\u5075\u6e2c\u5230\u9707\u52d5" + "is_bat_low": "{entity_name}\u96fb\u91cf\u904e\u4f4e", + "is_cold": "{entity_name}\u51b7", + "is_connected": "{entity_name}\u5df2\u9023\u7dda", + "is_gas": "{entity_name}\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_hot": "{entity_name}\u71b1", + "is_light": "{entity_name}\u5075\u6e2c\u5230\u5149\u7dda\u4e2d", + "is_locked": "{entity_name}\u5df2\u4e0a\u9396", + "is_moist": "{entity_name}\u6f6e\u6fd5", + "is_motion": "{entity_name}\u5075\u6e2c\u5230\u52d5\u4f5c\u4e2d", + "is_moving": "{entity_name}\u79fb\u52d5\u4e2d", + "is_no_gas": "{entity_name}\u672a\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_no_light": "{entity_name}\u672a\u5075\u6e2c\u5230\u5149\u7dda", + "is_no_motion": "{entity_name}\u672a\u5075\u6e2c\u5230\u52d5\u4f5c", + "is_no_problem": "{entity_name}\u672a\u5075\u6e2c\u5230\u554f\u984c", + "is_no_smoke": "{entity_name}\u672a\u5075\u6e2c\u5230\u7159\u9727", + "is_no_sound": "{entity_name}\u672a\u5075\u6e2c\u5230\u8072\u97f3", + "is_no_vibration": "{entity_name}\u672a\u5075\u6e2c\u5230\u9707\u52d5", + "is_not_bat_low": "{entity_name}\u96fb\u91cf\u6b63\u5e38", + "is_not_cold": "{entity_name}\u4e0d\u51b7", + "is_not_connected": "{entity_name}\u65b7\u7dda", + "is_not_hot": "{entity_name}\u4e0d\u71b1", + "is_not_locked": "{entity_name}\u89e3\u9396", + "is_not_moist": "{entity_name}\u4e7e\u71e5", + "is_not_moving": "{entity_name}\u672a\u5728\u79fb\u52d5", + "is_not_occupied": "{entity_name}\u672a\u6709\u4eba", + "is_not_open": "{entity_name}\u95dc\u9589", + "is_not_plugged_in": "{entity_name}\u672a\u63d2\u5165", + "is_not_powered": "{entity_name}\u672a\u901a\u96fb", + "is_not_present": "{entity_name}\u672a\u51fa\u73fe", + "is_not_unsafe": "{entity_name}\u5b89\u5168", + "is_occupied": "{entity_name}\u6709\u4eba", + "is_off": "{entity_name}\u95dc\u9589", + "is_on": "{entity_name}\u958b\u555f", + "is_open": "{entity_name}\u958b\u555f", + "is_plugged_in": "{entity_name}\u63d2\u5165", + "is_powered": "{entity_name}\u901a\u96fb", + "is_present": "{entity_name}\u51fa\u73fe", + "is_problem": "{entity_name}\u6b63\u5075\u6e2c\u5230\u554f\u984c", + "is_smoke": "{entity_name}\u6b63\u5075\u6e2c\u5230\u7159\u9727", + "is_sound": "{entity_name}\u6b63\u5075\u6e2c\u5230\u8072\u97f3", + "is_unsafe": "{entity_name}\u4e0d\u5b89\u5168", + "is_vibration": "{entity_name}\u6b63\u5075\u6e2c\u5230\u9707\u52d5" }, "trigger_type": { - "bat_low": "{entity_name} \u96fb\u91cf\u4f4e", - "closed": "{entity_name} \u5df2\u95dc\u9589", - "cold": "{entity_name} \u5df2\u8b8a\u51b7", - "connected": "{entity_name} \u5df2\u9023\u7dda", - "gas": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", - "hot": "{entity_name} \u5df2\u8b8a\u71b1", - "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", - "locked": "{entity_name} \u5df2\u4e0a\u9396", - "moist": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", - "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", - "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", - "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", - "no_gas": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", - "no_light": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u5149\u7dda", - "no_motion": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u52d5\u4f5c", - "no_problem": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", - "no_smoke": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", - "no_sound": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", - "no_vibration": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", - "not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", - "not_cold": "{entity_name} \u5df2\u4e0d\u51b7", - "not_connected": "{entity_name} \u5df2\u65b7\u7dda", - "not_hot": "{entity_name} \u5df2\u4e0d\u71b1", - "not_locked": "{entity_name} \u5df2\u89e3\u9396", - "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", - "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", - "not_occupied": "{entity_name} \u672a\u6709\u4eba", - "not_opened": "{entity_name} \u5df2\u95dc\u9589", - "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", - "not_powered": "{entity_name} \u672a\u901a\u96fb", - "not_present": "{entity_name} \u672a\u51fa\u73fe", - "not_unsafe": "{entity_name} \u5df2\u5b89\u5168", - "occupied": "{entity_name} \u8b8a\u6210\u6709\u4eba", - "opened": "{entity_name} \u5df2\u958b\u555f", - "plugged_in": "{entity_name} \u5df2\u63d2\u5165", - "powered": "{entity_name} \u5df2\u901a\u96fb", - "present": "{entity_name} \u5df2\u51fa\u73fe", - "problem": "{entity_name} \u5df2\u5075\u6e2c\u5230\u554f\u984c", - "smoke": "{entity_name} \u5df2\u5075\u6e2c\u5230\u7159\u9727", - "sound": "{entity_name} \u5df2\u5075\u6e2c\u5230\u8072\u97f3", - "turned_off": "{entity_name} \u5df2\u95dc\u9589", - "turned_on": "{entity_name} \u5df2\u958b\u555f", - "unsafe": "{entity_name} \u5df2\u4e0d\u5b89\u5168", - "vibration": "{entity_name} \u5df2\u5075\u6e2c\u5230\u9707\u52d5" + "bat_low": "{entity_name}\u96fb\u91cf\u4f4e", + "closed": "{entity_name}\u5df2\u95dc\u9589", + "cold": "{entity_name}\u5df2\u8b8a\u51b7", + "connected": "{entity_name}\u5df2\u9023\u7dda", + "gas": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", + "hot": "{entity_name}\u5df2\u8b8a\u71b1", + "light": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", + "locked": "{entity_name}\u5df2\u4e0a\u9396", + "moist": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", + "moist\u00a7": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", + "motion": "{entity_name}\u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", + "moving": "{entity_name}\u958b\u59cb\u79fb\u52d5", + "no_gas": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", + "no_light": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u5149\u7dda", + "no_motion": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u52d5\u4f5c", + "no_problem": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", + "no_smoke": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", + "no_sound": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", + "no_vibration": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", + "not_bat_low": "{entity_name}\u96fb\u91cf\u6b63\u5e38", + "not_cold": "{entity_name}\u5df2\u4e0d\u51b7", + "not_connected": "{entity_name}\u5df2\u65b7\u7dda", + "not_hot": "{entity_name}\u5df2\u4e0d\u71b1", + "not_locked": "{entity_name}\u5df2\u89e3\u9396", + "not_moist": "{entity_name}\u5df2\u8b8a\u4e7e", + "not_moving": "{entity_name}\u505c\u6b62\u79fb\u52d5", + "not_occupied": "{entity_name}\u672a\u6709\u4eba", + "not_opened": "{entity_name}\u5df2\u95dc\u9589", + "not_plugged_in": "{entity_name}\u672a\u63d2\u5165", + "not_powered": "{entity_name}\u672a\u901a\u96fb", + "not_present": "{entity_name}\u672a\u51fa\u73fe", + "not_unsafe": "{entity_name}\u5df2\u5b89\u5168", + "occupied": "{entity_name}\u8b8a\u6210\u6709\u4eba", + "opened": "{entity_name}\u5df2\u958b\u555f", + "plugged_in": "{entity_name}\u5df2\u63d2\u5165", + "powered": "{entity_name}\u5df2\u901a\u96fb", + "present": "{entity_name}\u5df2\u51fa\u73fe", + "problem": "{entity_name}\u5df2\u5075\u6e2c\u5230\u554f\u984c", + "smoke": "{entity_name}\u5df2\u5075\u6e2c\u5230\u7159\u9727", + "sound": "{entity_name}\u5df2\u5075\u6e2c\u5230\u8072\u97f3", + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f", + "unsafe": "{entity_name}\u5df2\u4e0d\u5b89\u5168", + "vibration": "{entity_name}\u5df2\u5075\u6e2c\u5230\u9707\u52d5" } } } \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/ca.json b/homeassistant/components/brother/.translations/ca.json index 62dd1807676..f927488e7e0 100644 --- a/homeassistant/components/brother/.translations/ca.json +++ b/homeassistant/components/brother/.translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Aquesta impressora ja est\u00e0 configurada.", "unsupported_model": "Aquest model d'impressora no \u00e9s compatible." }, "error": { @@ -8,6 +9,7 @@ "snmp_error": "El servidor SNMP s'ha tancat o la impressora no \u00e9s compatible.", "wrong_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids." }, + "flow_title": "Impressora Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -16,6 +18,12 @@ }, "description": "Configura la integraci\u00f3 d'impressora Brother. Si tens problemes amb la configuraci\u00f3, visita: https://www.home-assistant.io/integrations/brother", "title": "Impressora Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Tipus d'impressora" + }, + "title": "Impressora Brother descoberta" } }, "title": "Impressora Brother" diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json index 17e6c955046..28ff10f09f0 100644 --- a/homeassistant/components/climate/.translations/zh-Hant.json +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -1,16 +1,16 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", - "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" + "set_hvac_mode": "\u8b8a\u66f4{entity_name} HVAC \u6a21\u5f0f", + "set_preset_mode": "\u8b8a\u66f4{entity_name}\u8a2d\u5b9a\u6a21\u5f0f" }, "condition_type": { - "is_hvac_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", - "is_preset_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" + "is_hvac_mode": "{entity_name}\u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", + "is_preset_mode": "{entity_name}\u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" }, "trigger_type": { - "current_humidity_changed": "{entity_name} \u91cf\u6e2c\u6fd5\u5ea6\u5df2\u8b8a\u66f4", - "current_temperature_changed": "{entity_name} \u91cf\u6e2c\u6eab\u5ea6\u5df2\u8b8a\u66f4", + "current_humidity_changed": "{entity_name}\u91cf\u6e2c\u6fd5\u5ea6\u5df2\u8b8a\u66f4", + "current_temperature_changed": "{entity_name}\u91cf\u6e2c\u6eab\u5ea6\u5df2\u8b8a\u66f4", "hvac_mode_changed": "{entity_name} HVAC \u6a21\u5f0f\u5df2\u8b8a\u66f4" } } diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index f2880a72e61..790df01d9fc 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -1,20 +1,20 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} \u5df2\u95dc\u9589", - "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", - "is_open": "{entity_name} \u5df2\u958b\u555f", - "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", - "is_position": "\u76ee\u524d {entity_name} \u4f4d\u7f6e\u70ba", - "is_tilt_position": "\u76ee\u524d {entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" + "is_closed": "{entity_name}\u5df2\u95dc\u9589", + "is_closing": "{entity_name}\u6b63\u5728\u95dc\u9589", + "is_open": "{entity_name}\u5df2\u958b\u555f", + "is_opening": "{entity_name}\u6b63\u5728\u958b\u555f", + "is_position": "\u76ee\u524d{entity_name}\u4f4d\u7f6e\u70ba", + "is_tilt_position": "\u76ee\u524d{entity_name}\u6a19\u984c\u4f4d\u7f6e\u70ba" }, "trigger_type": { - "closed": "{entity_name} \u5df2\u95dc\u9589", - "closing": "{entity_name} \u6b63\u5728\u95dc\u9589", - "opened": "{entity_name} \u5df2\u958b\u555f", - "opening": "{entity_name} \u6b63\u5728\u958b\u555f", - "position": "{entity_name} \u4f4d\u7f6e\u8b8a\u66f4", - "tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u8b8a\u66f4" + "closed": "{entity_name}\u5df2\u95dc\u9589", + "closing": "{entity_name}\u6b63\u5728\u95dc\u9589", + "opened": "{entity_name}\u5df2\u958b\u555f", + "opening": "{entity_name}\u6b63\u5728\u958b\u555f", + "position": "{entity_name}\u4f4d\u7f6e\u8b8a\u66f4", + "tilt_position": "{entity_name}\u6a19\u984c\u4f4d\u7f6e\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index a51bfa056f6..8a9ae15a7c1 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", "remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades", + "remote_double_tap_any_side": "Dispositiu tocat dues vegades a alguna cara", "remote_falling": "Dispositiu en caiguda lliure", + "remote_flip_180_degrees": "Dispositiu voltejat 180 graus", + "remote_flip_90_degrees": "Dispositiu voltejat 90 graus", "remote_gyro_activated": "Dispositiu sacsejat", "remote_moved": "Dispositiu mogut amb la \"{subtype}\" amunt", + "remote_moved_any_side": "Dispositiu mogut amb alguna cara amunt", "remote_rotate_from_side_1": "Dispositiu rotat de la \"cara 1\" a la \"{subtype}\"", "remote_rotate_from_side_2": "Dispositiu rotat de la \"cara 2\" a la \"{subtype}\"", "remote_rotate_from_side_3": "Dispositiu rotat de la \"cara 3\" a la \"{subtype}\"", "remote_rotate_from_side_4": "Dispositiu rotat de la \"cara 4\" a la \"{subtype}\"", "remote_rotate_from_side_5": "Dispositiu rotat de la \"cara 5\" a la \"{subtype}\"", - "remote_rotate_from_side_6": "Dispositiu rotat de la \"cara 6\" a la \"{subtype}\"" + "remote_rotate_from_side_6": "Dispositiu rotat de la \"cara 6\" a la \"{subtype}\"", + "remote_turned_clockwise": "Dispositiu girat en sentit horari", + "remote_turned_counter_clockwise": "Dispositiu girat en sentit antihorari" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/zh-Hant.json b/homeassistant/components/device_tracker/.translations/zh-Hant.json index 456e09ebf0e..6611cb0c279 100644 --- a/homeassistant/components/device_tracker/.translations/zh-Hant.json +++ b/homeassistant/components/device_tracker/.translations/zh-Hant.json @@ -1,8 +1,8 @@ { "device_automation": { "condition_type": { - "is_home": "{entity_name} \u5728\u5bb6", - "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" + "is_home": "{entity_name}\u5728\u5bb6", + "is_not_home": "{entity_name}\u4e0d\u5728\u5bb6" } } } \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json index 78c0d991125..01da8652b2f 100644 --- a/homeassistant/components/fan/.translations/zh-Hant.json +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -1,16 +1,16 @@ { "device_automation": { "action_type": { - "turn_off": "\u95dc\u9589 {entity_name}", - "turn_on": "\u958b\u555f {entity_name}" + "turn_off": "\u95dc\u9589{entity_name}", + "turn_on": "\u958b\u555f{entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u95dc\u9589", - "is_on": "{entity_name} \u958b\u555f" + "is_off": "{entity_name}\u95dc\u9589", + "is_on": "{entity_name}\u958b\u555f" }, "trigger_type": { - "turned_off": "{entity_name} \u5df2\u95dc\u9589", - "turned_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/ca.json b/homeassistant/components/garmin_connect/.translations/ca.json new file mode 100644 index 00000000000..95e59cf350d --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest compte ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida.", + "too_many_requests": "Massa sol\u00b7licituds, torna-ho a intentar m\u00e9s tard.", + "unknown": "Error inesperat." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les teves credencials.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ca.json b/homeassistant/components/gios/.translations/ca.json index 80fedcafdd9..dadd38c24ae 100644 --- a/homeassistant/components/gios/.translations/ca.json +++ b/homeassistant/components/gios/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La integraci\u00f3 GIO\u015a per a aquesta estaci\u00f3 ja est\u00e0 configurada." + }, "error": { "cannot_connect": "No s'ha pogut connectar al servidor de GIO\u015a.", "invalid_sensors_data": "Les dades dels sensors d'aquesta estaci\u00f3 de mesura s\u00f3n inv\u00e0lides.", diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 5ac06129463..d8bda90de85 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "\u5207\u63db {entity_name}", - "turn_off": "\u95dc\u9589 {entity_name}", - "turn_on": "\u958b\u555f {entity_name}" + "toggle": "\u5207\u63db{entity_name}", + "turn_off": "\u95dc\u9589{entity_name}", + "turn_on": "\u958b\u555f{entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u5df2\u95dc\u9589", - "is_on": "{entity_name} \u5df2\u958b\u555f" + "is_off": "{entity_name}\u5df2\u95dc\u9589", + "is_on": "{entity_name}\u5df2\u958b\u555f" }, "trigger_type": { - "turned_off": "{entity_name} \u5df2\u95dc\u9589", - "turned_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ca.json b/homeassistant/components/linky/.translations/ca.json index ca437417f59..ff242c556fd 100644 --- a/homeassistant/components/linky/.translations/ca.json +++ b/homeassistant/components/linky/.translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja ha estat configurat", "username_exists": "El compte ja ha estat configurat" }, "error": { diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json index b5d69a21f9a..054f7a5a18d 100644 --- a/homeassistant/components/lock/.translations/zh-Hant.json +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "lock": "\u4e0a\u9396 {entity_name}", - "open": "\u958b\u555f {entity_name}", - "unlock": "\u89e3\u9396 {entity_name}" + "lock": "\u4e0a\u9396{entity_name}", + "open": "\u958b\u555f{entity_name}", + "unlock": "\u89e3\u9396{entity_name}" }, "condition_type": { - "is_locked": "{entity_name} \u5df2\u4e0a\u9396", - "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" + "is_locked": "{entity_name}\u5df2\u4e0a\u9396", + "is_unlocked": "{entity_name}\u5df2\u89e3\u9396" }, "trigger_type": { - "locked": "{entity_name} \u5df2\u4e0a\u9396", - "unlocked": "{entity_name} \u5df2\u89e3\u9396" + "locked": "{entity_name}\u5df2\u4e0a\u9396", + "unlocked": "{entity_name}\u5df2\u89e3\u9396" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/zh-Hant.json b/homeassistant/components/media_player/.translations/zh-Hant.json index abd2f75950b..e3353c5e5b9 100644 --- a/homeassistant/components/media_player/.translations/zh-Hant.json +++ b/homeassistant/components/media_player/.translations/zh-Hant.json @@ -1,11 +1,11 @@ { "device_automation": { "condition_type": { - "is_idle": "{entity_name} \u9592\u7f6e", - "is_off": "{entity_name} \u95dc\u9589", - "is_on": "{entity_name} \u958b\u555f", - "is_paused": "{entity_name} \u5df2\u66ab\u505c", - "is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e" + "is_idle": "{entity_name}\u9592\u7f6e", + "is_off": "{entity_name}\u95dc\u9589", + "is_on": "{entity_name}\u958b\u555f", + "is_paused": "{entity_name}\u5df2\u66ab\u505c", + "is_playing": "{entity_name}\u6b63\u5728\u64ad\u653e" } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/ca.json b/homeassistant/components/mikrotik/.translations/ca.json new file mode 100644 index 00000000000..acb9966d15d --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "La connexi\u00f3 no ha tingut \u00e8xit", + "name_exists": "El nom existeix", + "wrong_credentials": "Credencials incorrectes" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + } + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Activa el ping ARP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/da.json b/homeassistant/components/mikrotik/.translations/da.json new file mode 100644 index 00000000000..edaa47e52bb --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/da.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik er allerede konfigureret" + }, + "error": { + "cannot_connect": "Forbindelsen mislykkedes", + "name_exists": "Navnet findes allerede", + "wrong_credentials": "Forkerte legitimationsoplysninger" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "username": "Brugernavn", + "verify_ssl": "Brug ssl" + }, + "title": "Konfigurer Mikrotik-router" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Aktiver ARP-ping", + "detection_time": "Betragt som hjemme-interval", + "force_dhcp": "Gennemtving scanning ved hj\u00e6lp af DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/en.json b/homeassistant/components/mikrotik/.translations/en.json index 590563993d6..0423401bf83 100644 --- a/homeassistant/components/mikrotik/.translations/en.json +++ b/homeassistant/components/mikrotik/.translations/en.json @@ -1,35 +1,35 @@ { "config": { - "title": "Mikrotik", - "step": { - "user": { - "title": "Set up Mikrotik Router", - "data": { - "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port", - "verify_ssl": "Use ssl" - } - } - }, - "error": { - "name_exists": "Name exists", - "cannot_connect": "Connection Unsuccessful", - "wrong_credentials": "Wrong Credentials" - }, "abort": { "already_configured": "Mikrotik is already configured" - } + }, + "error": { + "cannot_connect": "Connection Unsuccessful", + "name_exists": "Name exists", + "wrong_credentials": "Wrong Credentials" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "username": "Username", + "verify_ssl": "Use ssl" + }, + "title": "Set up Mikrotik Router" + } + }, + "title": "Mikrotik" }, "options": { "step": { "device_tracker": { "data": { "arp_ping": "Enable ARP ping", - "force_dhcp": "Force scanning using DHCP", - "detection_time": "Consider home interval" + "detection_time": "Consider home interval", + "force_dhcp": "Force scanning using DHCP" } } } diff --git a/homeassistant/components/mikrotik/.translations/ru.json b/homeassistant/components/mikrotik/.translations/ru.json new file mode 100644 index 00000000000..844181b5b64 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d", + "verify_ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL" + }, + "title": "MikroTik" + } + }, + "title": "MikroTik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c ARP-\u043f\u0438\u043d\u0433", + "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", + "force_dhcp": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/ca.json b/homeassistant/components/netatmo/.translations/ca.json index 6961db6f520..63de8699f35 100644 --- a/homeassistant/components/netatmo/.translations/ca.json +++ b/homeassistant/components/netatmo/.translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte Netatmo.", - "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3." + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "El component Netatmo no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Netatmo." diff --git a/homeassistant/components/point/.translations/ca.json b/homeassistant/components/point/.translations/ca.json index fd603aa0430..c4d9228532d 100644 --- a/homeassistant/components/point/.translations/ca.json +++ b/homeassistant/components/point/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un compte de Point.", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", - "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "S'ha acabat el temps d'espera 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": "Necessites configurar Point abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/point/)." }, diff --git a/homeassistant/components/ring/.translations/ca.json b/homeassistant/components/ring/.translations/ca.json index d51de2b8667..6f549f8ef28 100644 --- a/homeassistant/components/ring/.translations/ca.json +++ b/homeassistant/components/ring/.translations/ca.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat" }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json index beeb62d8bdb..78581987df8 100644 --- a/homeassistant/components/samsungtv/.translations/ca.json +++ b/homeassistant/components/samsungtv/.translations/ca.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "La Samsung TV ja configurada.", + "already_in_progress": "La configuraci\u00f3 de la Samsung TV ja est\u00e0 en curs.", + "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquesta Samsung TV.", + "not_found": "No s'han trobat Samsung TV's compatibles a la xarxa.", + "not_supported": "Actualment aquest dispositiu Samsung TV no \u00e9s compatible." + }, "step": { "confirm": { "title": "Samsung TV" diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json index eb8f47a1fd9..9bf8abc8230 100644 --- a/homeassistant/components/sensor/.translations/zh-Hant.json +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\u76ee\u524d {entity_name} \u96fb\u91cf", - "is_humidity": "\u76ee\u524d {entity_name} \u6fd5\u5ea6", - "is_illuminance": "\u76ee\u524d {entity_name} \u7167\u5ea6", - "is_power": "\u76ee\u524d {entity_name} \u96fb\u529b", - "is_pressure": "\u76ee\u524d {entity_name} \u58d3\u529b", - "is_signal_strength": "\u76ee\u524d {entity_name} \u8a0a\u865f\u5f37\u5ea6", - "is_temperature": "\u76ee\u524d {entity_name} \u6eab\u5ea6", - "is_timestamp": "\u76ee\u524d {entity_name} \u6642\u9593\u6a19\u8a18", - "is_value": "\u76ee\u524d {entity_name} \u503c" + "is_battery_level": "\u76ee\u524d{entity_name}\u96fb\u91cf", + "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", + "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", + "is_power": "\u76ee\u524d{entity_name}\u96fb\u529b", + "is_pressure": "\u76ee\u524d{entity_name}\u58d3\u529b", + "is_signal_strength": "\u76ee\u524d{entity_name}\u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "\u76ee\u524d{entity_name}\u6eab\u5ea6", + "is_timestamp": "\u76ee\u524d{entity_name}\u6642\u9593\u6a19\u8a18", + "is_value": "\u76ee\u524d{entity_name}\u503c" }, "trigger_type": { - "battery_level": "{entity_name} \u96fb\u91cf\u8b8a\u66f4", - "humidity": "{entity_name} \u6fd5\u5ea6\u8b8a\u66f4", - "illuminance": "{entity_name} \u7167\u5ea6\u8b8a\u66f4", - "power": "{entity_name} \u96fb\u529b\u8b8a\u66f4", - "pressure": "{entity_name} \u58d3\u529b\u8b8a\u66f4", - "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", - "temperature": "{entity_name} \u6eab\u5ea6\u8b8a\u66f4", - "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18\u8b8a\u66f4", - "value": "{entity_name} \u503c\u8b8a\u66f4" + "battery_level": "{entity_name}\u96fb\u91cf\u8b8a\u66f4", + "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", + "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", + "power": "{entity_name}\u96fb\u529b\u8b8a\u66f4", + "pressure": "{entity_name}\u58d3\u529b\u8b8a\u66f4", + "signal_strength": "{entity_name}\u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", + "temperature": "{entity_name}\u6eab\u5ea6\u8b8a\u66f4", + "timestamp": "{entity_name}\u6642\u9593\u6a19\u8a18\u8b8a\u66f4", + "value": "{entity_name}\u503c\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index a1a5b9489fa..00bc3eef39c 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Soma.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "connection_error": "No s'ha pogut connectar amb SOMA Connect.", "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3.", diff --git a/homeassistant/components/somfy/.translations/ca.json b/homeassistant/components/somfy/.translations/ca.json index b3095cd4e9c..58b8853cd51 100644 --- a/homeassistant/components/somfy/.translations/ca.json +++ b/homeassistant/components/somfy/.translations/ca.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s pots configurar un compte de Somfy.", - "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Somfy.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component Somfy no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, "create_entry": { diff --git a/homeassistant/components/spotify/.translations/ca.json b/homeassistant/components/spotify/.translations/ca.json new file mode 100644 index 00000000000..fa0fa734353 --- /dev/null +++ b/homeassistant/components/spotify/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Spotify.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "La integraci\u00f3 Spotify no est\u00e0 configurada. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Spotify." + }, + "step": { + "pick_implementation": { + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index 517d48354dc..3eaac840497 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -1,19 +1,19 @@ { "device_automation": { "action_type": { - "toggle": "\u5207\u63db {entity_name}", - "turn_off": "\u95dc\u9589 {entity_name}", - "turn_on": "\u958b\u555f {entity_name}" + "toggle": "\u5207\u63db{entity_name}", + "turn_off": "\u95dc\u9589{entity_name}", + "turn_on": "\u958b\u555f{entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u5df2\u95dc\u9589", - "is_on": "{entity_name} \u5df2\u958b\u555f", - "turn_off": "{entity_name} \u5df2\u95dc\u9589", - "turn_on": "{entity_name} \u5df2\u958b\u555f" + "is_off": "{entity_name}\u5df2\u95dc\u9589", + "is_on": "{entity_name}\u5df2\u958b\u555f", + "turn_off": "{entity_name}\u5df2\u95dc\u9589", + "turn_on": "{entity_name}\u5df2\u958b\u555f" }, "trigger_type": { - "turned_off": "{entity_name} \u5df2\u95dc\u9589", - "turned_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index a337474c96b..6f337d9a4d3 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "TelldusLive ja est\u00e0 configurat", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", - "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "unknown": "S'ha produ\u00eft un error desconegut" }, "error": { diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json index b406e1baede..b108a2a6a44 100644 --- a/homeassistant/components/vacuum/.translations/zh-Hant.json +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -1,16 +1,16 @@ { "device_automation": { "action_type": { - "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", - "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" + "clean": "\u555f\u52d5{entity_name}\u6e05\u9664", + "dock": "\u555f\u52d5{entity_name}\u56de\u5230\u5145\u96fb\u7ad9" }, "condition_type": { - "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u6383", - "is_docked": "{entity_name} \u65bc\u5145\u96fb\u7ad9" + "is_cleaning": "{entity_name}\u6b63\u5728\u6e05\u6383", + "is_docked": "{entity_name}\u65bc\u5145\u96fb\u7ad9" }, "trigger_type": { - "cleaning": "{entity_name} \u958b\u59cb\u6e05\u6383", - "docked": "{entity_name} \u5df2\u56de\u5145\u96fb\u7ad9" + "cleaning": "{entity_name}\u958b\u59cb\u6e05\u6383", + "docked": "{entity_name}\u5df2\u56de\u5145\u96fb\u7ad9" } } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index abbf1092bf3..28f922f9e33 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Aquesta entrada ja ha estat configurada." + }, "error": { "host_exists": "L'amfitri\u00f3 ja est\u00e0 configurat.", "name_exists": "El nom ja est\u00e0 configurat." @@ -21,6 +24,7 @@ "step": { "init": { "data": { + "timeout": "Temps d'espera de les sol\u00b7licituds API (en segons)", "volume_step": "Mida del pas de volum" } } diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index 5794dbbc1a5..edb95a946aa 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3.", "no_flows": "Necessites configurar Withings abans de poder autenticar't-hi. Llegeix la documentaci\u00f3." }, "create_entry": { From 7e9507833b5be1a5db71876b841b31a5a1e5d964 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 31 Jan 2020 01:28:54 -0600 Subject: [PATCH 023/378] Fix async bug in amcrest when registering services (#31334) --- homeassistant/components/amcrest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 63daeb04731..f7814939e3a 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -256,7 +256,7 @@ def setup(hass, config): async_dispatcher_send(hass, service_signal(call.service, entity_id), *args) for service, params in CAMERA_SERVICES.items(): - hass.services.async_register(DOMAIN, service, async_service_handler, params[0]) + hass.services.register(DOMAIN, service, async_service_handler, params[0]) return True From b22dfa119baabbdd1f41f1d49d82f560e9cbf5a6 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 31 Jan 2020 08:30:13 +0100 Subject: [PATCH 024/378] Emulated Hue: changed fallback device-type to fix Alexa compatibility issues (#30013) (#31330) * Emulated Hue: changed the reported fallback device-type to fix Alexa compatibility issues (#30013) * Emulated Hue: updated tests (#30013) --- homeassistant/components/emulated_hue/hue_api.py | 7 ++++--- tests/components/emulated_hue/test_hue_api.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 459a13c066c..118bf7e3eaa 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -704,9 +704,10 @@ def entity_to_json(config, entity): retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) else: - # On/off light (Zigbee Device ID: 0x0000) - # Supports groups, scenes and on/off control - retval["type"] = "On/off light" + # On/off plug-in unit (Zigbee Device ID: 0x0000) + # Supports groups and on/off control + # Used for compatibility purposes with Alexa instead of "On/off light" + retval["type"] = "On/off plug-in unit" retval["modelid"] = "HASS321" return retval diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 349d53aaee5..0ddc429b2d9 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -238,7 +238,7 @@ async def test_light_without_brightness_supported(hass_hue, hue_client): ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True - assert light_without_brightness_json["type"] == "On/off light" + assert light_without_brightness_json["type"] == "On/off plug-in unit" async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): From d405069406955c7bd88f500c59617c0c73690c11 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 00:32:43 -0800 Subject: [PATCH 025/378] Guard for callbacks in service helper (#31339) --- homeassistant/components/camera/__init__.py | 10 ++++------ homeassistant/helpers/service.py | 8 ++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 53c5cf16a98..b02874780e5 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -400,19 +400,17 @@ class Camera(Entity): """Turn off camera.""" raise NotImplementedError() - @callback - def async_turn_off(self): + async def async_turn_off(self): """Turn off camera.""" - return self.hass.async_add_job(self.turn_off) + await self.hass.async_add_job(self.turn_off) def turn_on(self): """Turn off camera.""" raise NotImplementedError() - @callback - def async_turn_on(self): + async def async_turn_on(self): """Turn off camera.""" - return self.hass.async_add_job(self.turn_on) + await self.hass.async_add_job(self.turn_on) def enable_motion_detection(self): """Enable motion detection in the camera.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 89c2715a760..36bfd9c8cb0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -370,9 +370,13 @@ async def _handle_service_platform_call( entity.async_set_context(context) if isinstance(func, str): - result = await hass.async_add_job(partial(getattr(entity, func), **data)) + result = hass.async_add_job(partial(getattr(entity, func), **data)) else: - result = await hass.async_add_job(func, entity, data) + result = hass.async_add_job(func, entity, data) + + # Guard because callback functions do not return a task when passed to async_add_job. + if result is not None: + result = await result if asyncio.iscoroutine(result): _LOGGER.error( From 6a7bb7b1498c86d40f367ae020b79e7cbeda09a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 01:58:27 -0800 Subject: [PATCH 026/378] Fix incorrect annotation async flock notify (#31342) * Fix incorrect annotation async flock notify * Update notify.py * Update notify.py Co-authored-by: Pascal Vizeli --- homeassistant/components/flock/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index a71601ea2c4..107c837970d 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -16,7 +16,7 @@ _RESOURCE = "https://api.flock.com/hooks/sendMessage/" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_ACCESS_TOKEN): cv.string}) -async def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Flock notification service.""" access_token = config.get(CONF_ACCESS_TOKEN) url = f"{_RESOURCE}{access_token}" From a0067a298ac89b98e6baad536460abc22c6e3826 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 31 Jan 2020 06:45:23 -0500 Subject: [PATCH 027/378] Remove Throttle on async_setup and bump pyvizio version (#31337) --- homeassistant/components/vizio/const.py | 5 ----- homeassistant/components/vizio/manifest.json | 2 +- homeassistant/components/vizio/media_player.py | 4 ---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 92fb37c153e..e3ac66e05c3 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,6 +1,4 @@ """Constants used by vizio component.""" -from datetime import timedelta - from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, @@ -72,6 +70,3 @@ VIZIO_SCHEMA = { vol.Coerce(int), vol.Range(min=1, max=10) ), } - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 2f3a581e113..7f397a4ed0c 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.4"], + "requirements": ["pyvizio==0.1.16"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 3ea70fe2acb..439a9a972d4 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -5,7 +5,6 @@ from typing import Callable, List from pyvizio import VizioAsync -from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -32,8 +31,6 @@ from .const import ( DEVICE_ID, DOMAIN, ICON, - MIN_TIME_BETWEEN_FORCED_SCANS, - MIN_TIME_BETWEEN_SCANS, SUPPORTED_COMMANDS, VIZIO_DEVICE_CLASSES, ) @@ -111,7 +108,6 @@ class VizioDevice(MediaPlayerDevice): self._icon = ICON[device_class] self._available = True - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) async def async_update(self) -> None: """Retrieve latest state of the device.""" is_on = await self._device.get_power_state(log_api_exception=False) diff --git a/requirements_all.txt b/requirements_all.txt index ca276bf8035..4c45014c12e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.4 +pyvizio==0.1.16 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a8a794b429..abf8379932a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.4 +pyvizio==0.1.16 # homeassistant.components.html5 pywebpush==1.9.2 From ab3157e661255b7cb65bf6775441cc69d8b078d9 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 31 Jan 2020 18:25:54 +0200 Subject: [PATCH 028/378] Upgrade pysma, fix #27154 (#31346) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 1c4b98c2911..a56fe7ab151 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -2,7 +2,7 @@ "domain": "sma", "name": "SMA Solar", "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.3.4"], + "requirements": ["pysma==0.3.5"], "dependencies": [], "codeowners": ["@kellerza"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4c45014c12e..957e54ab4f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1497,7 +1497,7 @@ pysher==1.0.1 pysignalclirestapi==0.1.4 # homeassistant.components.sma -pysma==0.3.4 +pysma==0.3.5 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abf8379932a..6b509c2dbc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -516,7 +516,7 @@ pyps4-2ndscreen==1.0.6 pyqwikswitch==0.93 # homeassistant.components.sma -pysma==0.3.4 +pysma==0.3.5 # homeassistant.components.smartthings pysmartapp==0.3.2 From a017c262349e0e34da7e302f223261ecb06bd06b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 31 Jan 2020 17:27:16 +0100 Subject: [PATCH 029/378] Partially Revert "Deprecate hide_if_away from device_tracker (#30833) (#31348) --- .../components/device_tracker/legacy.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 6d343de8cb2..da3c945bc86 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -519,24 +519,21 @@ async def async_load_config( This method is a coroutine. """ - dev_schema = vol.All( - cv.deprecated(CONF_AWAY_HIDE, invalidation_version="0.107.0"), - vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), - vol.Optional("track", default=False): cv.boolean, - vol.Optional(CONF_MAC, default=None): vol.Any( - None, vol.All(cv.string, vol.Upper) - ), - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - vol.Optional("gravatar", default=None): vol.Any(None, cv.string), - vol.Optional("picture", default=None): vol.Any(None, cv.string), - vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( - cv.time_period, cv.positive_timedelta - ), - } - ), + dev_schema = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), + vol.Optional("track", default=False): cv.boolean, + vol.Optional(CONF_MAC, default=None): vol.Any( + None, vol.All(cv.string, vol.Upper) + ), + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + vol.Optional("gravatar", default=None): vol.Any(None, cv.string), + vol.Optional("picture", default=None): vol.Any(None, cv.string), + vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( + cv.time_period, cv.positive_timedelta + ), + } ) result = [] try: From df7d2b3aeb52d56fed42a9fdd0818059b85f31b3 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 31 Jan 2020 17:33:00 +0100 Subject: [PATCH 030/378] Fix typos found by codespell (#31243) * Fix typos found by codespell * Fix typos found by codespell * codespell: Furture ==> Future * Update test_config_flow.py * Update __init__.py * Spellcheck: successfull ==> successful * Codespell: unsuccesful ==> unsuccessful * Codespell: cant ==> can't * Codespell: firware ==> firmware * Codespell: mimick ==> mimic --- .pre-commit-config.yaml | 9 +++++++++ azure-pipelines-ci.yml | 4 ++++ azure-pipelines-release.yml | 2 +- homeassistant/auth/__init__.py | 2 +- homeassistant/auth/mfa_modules/__init__.py | 2 +- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/bootstrap.py | 2 +- homeassistant/components/alexa/messages.py | 2 +- homeassistant/components/amcrest/binary_sensor.py | 2 +- homeassistant/components/amcrest/camera.py | 2 +- homeassistant/components/amcrest/sensor.py | 2 +- homeassistant/components/apns/notify.py | 2 +- .../components/binary_sensor/device_condition.py | 2 +- homeassistant/components/bluesound/media_player.py | 2 +- homeassistant/components/bom/sensor.py | 2 +- homeassistant/components/broadlink/remote.py | 2 +- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/cert_expiry/config_flow.py | 2 +- homeassistant/components/climate/services.yaml | 2 +- homeassistant/components/configurator/__init__.py | 2 +- homeassistant/components/emulated_hue/hue_api.py | 2 +- homeassistant/components/emulated_roku/binding.py | 2 +- homeassistant/components/evohome/__init__.py | 6 +++--- homeassistant/components/fan/__init__.py | 2 +- homeassistant/components/fibaro/light.py | 2 +- homeassistant/components/filter/sensor.py | 2 +- homeassistant/components/freebox/switch.py | 2 +- homeassistant/components/garmin_connect/__init__.py | 12 ++++++------ homeassistant/components/garmin_connect/sensor.py | 4 ++-- homeassistant/components/google_assistant/http.py | 2 +- homeassistant/components/hdmi_cec/services.yaml | 2 +- homeassistant/components/homekit/type_lights.py | 2 +- .../components/homekit_controller/__init__.py | 4 ++-- .../components/homekit_controller/connection.py | 4 ++-- homeassistant/components/homematicip_cloud/sensor.py | 4 ++-- homeassistant/components/insteon/utils.py | 4 ++-- homeassistant/components/ios/__init__.py | 2 +- homeassistant/components/izone/discovery.py | 4 ++-- homeassistant/components/lcn/services.py | 2 +- homeassistant/components/logger/__init__.py | 2 +- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/met/config_flow.py | 8 ++++---- homeassistant/components/mfi/switch.py | 2 +- homeassistant/components/mikrotik/hub.py | 2 +- homeassistant/components/pi_hole/__init__.py | 2 +- homeassistant/components/pi_hole/const.py | 2 +- homeassistant/components/prometheus/__init__.py | 2 +- homeassistant/components/rmvtransport/sensor.py | 2 +- homeassistant/components/saj/sensor.py | 2 +- homeassistant/components/sigfox/sensor.py | 2 +- homeassistant/components/smappee/__init__.py | 4 ++-- homeassistant/components/solaredge/config_flow.py | 2 +- homeassistant/components/solarlog/config_flow.py | 2 +- homeassistant/components/stream/worker.py | 2 +- homeassistant/components/tod/binary_sensor.py | 4 ++-- homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/tradfri/light.py | 2 +- homeassistant/components/transmission/switch.py | 2 +- homeassistant/components/utility_meter/sensor.py | 2 +- homeassistant/components/vacuum/__init__.py | 4 ++-- homeassistant/components/velbus/config_flow.py | 2 +- homeassistant/components/water_heater/__init__.py | 2 +- homeassistant/components/wink/services.yaml | 6 +++--- homeassistant/components/yamaha/services.yaml | 2 +- homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zhong_hong/climate.py | 2 +- homeassistant/components/zwave/climate.py | 4 ++-- homeassistant/components/zwave/lock.py | 2 +- homeassistant/config_entries.py | 2 +- homeassistant/data_entry_flow.py | 2 +- homeassistant/helpers/logging.py | 2 +- homeassistant/helpers/template.py | 2 +- homeassistant/util/package.py | 2 +- script/scaffold/gather_info.py | 6 +++--- tests/auth/mfa_modules/test_notify.py | 2 +- tests/auth/test_init.py | 2 +- tests/components/api/test_init.py | 4 ++-- tests/components/apprise/test_notify.py | 4 ++-- tests/components/arcam_fmj/test_media_player.py | 6 +++--- tests/components/bayesian/test_binary_sensor.py | 2 +- tests/components/cert_expiry/test_config_flow.py | 2 +- tests/components/cloud/test_client.py | 2 +- tests/components/config/test_zwave.py | 6 +++--- tests/components/darksky/test_sensor.py | 2 +- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/facebook/test_notify.py | 2 +- tests/components/fritzbox/test_climate.py | 2 +- tests/components/heos/test_media_player.py | 2 +- tests/components/homekit/test_accessories.py | 2 +- .../homekit_controller/test_alarm_control_panel.py | 2 +- tests/components/homekit_controller/test_lock.py | 2 +- tests/components/hue/test_config_flow.py | 2 +- tests/components/input_datetime/test_init.py | 2 +- tests/components/mikrotik/test_config_flow.py | 2 +- tests/components/mikrotik/test_init.py | 2 +- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_discovery.py | 2 +- tests/components/ring/test_light.py | 2 +- tests/components/ring/test_switch.py | 2 +- tests/components/smhi/test_weather.py | 2 +- tests/components/solaredge/test_config_flow.py | 2 +- tests/components/solarlog/test_config_flow.py | 2 +- tests/components/stream/test_hls.py | 4 ++-- tests/components/tellduslive/test_config_flow.py | 2 +- tests/components/template/test_cover.py | 2 +- tests/components/tod/test_binary_sensor.py | 2 +- tests/components/unifi/test_controller.py | 2 +- tests/components/unifi/test_device_tracker.py | 6 +++--- tests/components/velbus/test_config_flow.py | 2 +- tests/components/vizio/conftest.py | 2 +- tests/components/vizio/test_config_flow.py | 2 +- tests/components/yessssms/test_notify.py | 2 +- tests/components/zha/test_sensor.py | 2 +- tests/components/zwave/test_climate.py | 2 +- tests/components/zwave/test_init.py | 2 +- tests/helpers/test_entity_component.py | 4 ++-- tests/helpers/test_entity_platform.py | 4 ++-- tests/helpers/test_event.py | 6 +++--- tests/helpers/test_service.py | 4 ++-- tests/test_config.py | 2 +- tox.ini | 1 + 121 files changed, 170 insertions(+), 156 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f27e82b6d9..05601865691 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,15 @@ repos: - --safe - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ +- repo: https://github.com/codespell-project/codespell + rev: v1.16.0 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --skip="./.*,*.json" + - --quiet-level=2 + exclude_types: [json] - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 546b63950fe..0474bf77489 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -44,6 +44,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt pre-commit install-hooks --config .pre-commit-config-all.yaml + - script: | + . venv/bin/activate + pre-commit run codespell --all-files + displayName: 'Run codespell' - script: | . venv/bin/activate pre-commit run flake8 --all-files diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 135057f2ae4..c98f12dfac6 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -163,7 +163,7 @@ stages: git commit -am "Bump Home Assistant $version" git push - displayName: 'Update version files' + displayName: "Update version files" - job: 'ReleaseDocker' pool: vmImage: 'ubuntu-latest' diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 9b3cf49fa22..710b4af1cd8 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -301,7 +301,7 @@ class AuthManager: async def async_deactivate_user(self, user: models.User) -> None: """Deactivate a user.""" if user.is_owner: - raise ValueError("Unable to deactive the owner") + raise ValueError("Unable to deactivate the owner") await self._store.async_deactivate_user(user) async def async_remove_credentials(self, credentials: models.Credentials) -> None: diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index fd9e61b9d17..c2ec2260cf2 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -1,4 +1,4 @@ -"""Plugable auth modules for Home Assistant.""" +"""Pluggable auth modules for Home Assistant.""" import importlib import logging import types diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 46cc634bcae..8da81a44a61 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -317,7 +317,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_setup( self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: - """Verify user can recevie one-time password.""" + """Verify user can receive one-time password.""" errors: Dict[str, str] = {} hass = self._auth_module.hass diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3d8523bf9ac..0b7dbe370be 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -193,7 +193,7 @@ def async_enable_logging( pass # If the above initialization failed for any reason, setup the default - # formatting. If the above succeeds, this wil result in a no-op. + # formatting. If the above succeeds, this will result in a no-op. logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) # Suppress overly verbose logs from libraries that aren't helpful diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index cb78f269f8f..4dd154ea11f 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -43,7 +43,7 @@ class AlexaDirective: Behavior when self.has_endpoint is False is undefined. Will raise AlexaInvalidEndpointError if the endpoint in the request is - malformed or nonexistant. + malformed or nonexistent. """ _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] self.entity_id = _endpoint_id.replace("#", ".") diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index ac16f0664aa..a99901f54a3 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -1,4 +1,4 @@ -"""Suppoort for Amcrest IP camera binary sensors.""" +"""Support for Amcrest IP camera binary sensors.""" from datetime import timedelta import logging diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index e9e1e2b5f84..8ff6403c566 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -121,7 +121,7 @@ class AmcrestCam(Camera): available = self.available if not available or not self.is_on: _LOGGER.warning( - "Attempt to take snaphot when %s camera is %s", + "Attempt to take snapshot when %s camera is %s", self.name, "offline" if not available else "off", ) diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index be03b3bedff..04436cd95ab 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -1,4 +1,4 @@ -"""Suppoort for Amcrest IP camera sensors.""" +"""Support for Amcrest IP camera sensors.""" from datetime import timedelta import logging diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index febe344a9c4..3cd43ee36ae 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -177,7 +177,7 @@ class ApnsNotificationService(BaseNotificationService): def device_state_changed_listener(self, entity_id, from_s, to_s): """ - Listen for sate change. + Listen for state change. Track device state change if a device has a tracking id specified. """ diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 63f84b657c1..cb98ec90b5d 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,4 +1,4 @@ -"""Implemenet device conditions for binary sensor.""" +"""Implement device conditions for binary sensor.""" from typing import Dict, List import voluptuous as vol diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 04ba21555d4..db5c65eab8b 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -421,7 +421,7 @@ class BluesoundPlayer(MediaPlayerDevice): # sync_status. We will force an update if the player is # grouped this isn't a foolproof solution. A better # solution would be to fetch sync_status more often when - # the device is playing. This would solve alot of + # the device is playing. This would solve a lot of # problems. This change will be done when the # communication is moved to a separate library await self.force_update_sync_status() diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 7d951968cb2..4728e22c877 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -281,7 +281,7 @@ def _get_bom_stations(): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. This function does several MB of internet requests, so please use the - caching version to minimise latency and hit-count. + caching version to minimize latency and hit-count. """ latlon = {} with io.BytesIO() as file_obj: diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 0b9d10b1e74..96698e5b02f 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -270,7 +270,7 @@ class BroadlinkRemote(RemoteDevice): async def _async_learn_code(self, command, device, toggle, timeout): """Learn a code from a remote. - Capture an aditional code for toggle commands. + Capture an additional code for toggle commands. """ try: if not toggle: diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b02874780e5..647e54556c4 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -377,7 +377,7 @@ class Camera(Entity): async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera. - This method can be overridden by camera plaforms to proxy + This method can be overridden by camera platforms to proxy a direct stream from the camera. """ return await self.handle_async_still_stream(request, self.frame_interval) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 14532aea65f..f3bd2f07d63 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -69,7 +69,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: # set some defaults in case we need to return to the form diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 34e89d57346..815df57f342 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -30,7 +30,7 @@ set_temperature: description: New target temperature for HVAC. example: 25 target_temp_high: - description: New target high tempereature for HVAC. + description: New target high temperature for HVAC. example: 26 target_temp_low: description: New target low temperature for HVAC. diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 78333d96355..4d79b13355c 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -209,7 +209,7 @@ class Configurator: entity_id = self._requests.pop(request_id)[0] # If we remove the state right away, it will not be included with - # the result fo the service call (current design limitation). + # the result of the service call (current design limitation). # Instead, we will set it to configured to give as feedback but delete # it shortly after so that it is deleted when the client updates. self.hass.states.async_set(entity_id, STATE_CONFIGURED) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 118bf7e3eaa..882fafe630c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -720,7 +720,7 @@ def create_hue_success_response(entity_id, attr, value): def create_list_of_entities(config, request): - """Create a list of all entites.""" + """Create a list of all entities.""" hass = request.app["hass"] json_response = {} diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index a44effff55a..1d233c9ed81 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -109,7 +109,7 @@ class EmulatedRoku: ) LOGGER.debug( - "Intializing emulated_roku %s on %s:%s", + "Initializing emulated_roku %s on %s:%s", self.roku_usn, self.host_ip, self.listen_port, diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 949471d64d0..1a408c0a660 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -148,7 +148,7 @@ def _handle_exception(err) -> bool: return False except aiohttp.ClientConnectionError: - # this appears to be a common occurance with the vendor's servers + # this appears to be a common occurrence with the vendor's servers _LOGGER.warning( "Unable to connect with the vendor's server. " "Check your network and the vendor's service status page. " @@ -184,7 +184,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: tokens = dict(app_storage if app_storage else {}) if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: - # any tokens wont be valid, and store might be be corrupt + # any tokens won't be valid, and store might be be corrupt await store.async_save({}) return ({}, None) @@ -266,7 +266,7 @@ def setup_service_functions(hass: HomeAssistantType, broker): Not all Honeywell TCC-compatible systems support all operating modes. In addition, each mode will require any of four distinct service schemas. This has to be - enumerated before registering the approperiate handlers. + enumerated before registering the appropriate handlers. It appears that all TCC-compatible systems support the same three zones modes. """ diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 38234a8f832..76bd16a6363 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -169,7 +169,7 @@ class FanEntity(ToggleEntity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" return {ATTR_SPEED_LIST: self.speed_list} @property diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index ba77942a448..38779a05cb0 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -68,7 +68,7 @@ class FibaroLight(FibaroDevice, Light): supports_dimming = "levelChange" in fibaro_device.interfaces supports_white_v = "setW" in fibaro_device.actions - # Configuration can overrride default capability detection + # Configuration can override default capability detection if devconf.get(CONF_DIMMING, supports_dimming): self._supported_flags |= SUPPORT_BRIGHTNESS if devconf.get(CONF_COLOR, supports_color): diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index baa4f90af3f..77622f62b1d 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -377,7 +377,7 @@ class Filter: @property def skip_processing(self): - """Return wether the current filter_state should be skipped.""" + """Return whether the current filter_state should be skipped.""" return self._skip_processing def _filter_state(self, new_state): diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index b6655c9634f..062d6a699fe 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -18,7 +18,7 @@ class FbxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" def __init__(self, fbx): - """Initilize the Wifi switch.""" + """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None self._fbx = fbx diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 5336394f671..d63d82d1284 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -43,13 +43,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occured during Garmin Connect login: %s", err) + _LOGGER.error("Error occurred during Garmin Connect login: %s", err) return False except (GarminConnectConnectionError) as err: - _LOGGER.error("Error occured during Garmin Connect login: %s", err) + _LOGGER.error("Error occurred during Garmin Connect login: %s", err) raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occured during Garmin Connect login") + _LOGGER.error("Unknown error occurred during Garmin Connect login") return False garmin_data = GarminConnectData(hass, garmin_client) @@ -98,11 +98,11 @@ class GarminConnectData: GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occured during Garmin Connect get stats: %s", err) + _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err) return except (GarminConnectConnectionError) as err: - _LOGGER.error("Error occured during Garmin Connect get stats: %s", err) + _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err) return except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occured during Garmin Connect get stats") + _LOGGER.error("Unknown error occurred during Garmin Connect get stats") return diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 6a3128cae01..51ec421e02b 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -32,9 +32,9 @@ async def async_setup_entry( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occured during Garmin Connect Client update: %s", err) + _LOGGER.error("Error occurred during Garmin Connect Client update: %s", err) except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occured during Garmin Connect Client update.") + _LOGGER.error("Unknown error occurred during Garmin Connect Client update.") entities = [] for ( diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 7bd3583e5c2..60e4cdae6a5 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -180,7 +180,7 @@ class GoogleConfig(AbstractConfig): return 500 async def async_call_homegraph_api(self, url, data): - """Call a homegraph api with authenticaiton.""" + """Call a homegraph api with authentication.""" session = async_get_clientsession(self.hass) async def _call(): diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index f2e5f0b837a..63aee668062 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -3,7 +3,7 @@ select_device: description: Select HDMI device. fields: device: {description: 'Address of device to select. Can be entity_id, physical - address or alias from confuguration.', example: '"switch.hdmi_1" or "1.1.0.0" + address or alias from configuration.', example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"'} send_command: description: Sends CEC command into HDMI CEC capable adapter. diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 3fc6a0628ff..734568606b2 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -201,7 +201,7 @@ class Light(HomeAccessory): # But if it is set to 0, HomeKit will update the brightness to 100 as # it thinks 0 is off. # - # Therefore, if the the brighness is 0 and the device is still on, + # Therefore, if the the brightness is 0 and the device is still on, # the brightness is mapped to 1 otherwise the update is ignored in # order to avoid this incorrect behavior. if brightness == 0: diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index dc65796a569..b2275282293 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -63,7 +63,7 @@ class HomeKitEntity(Entity): return False def setup(self): - """Configure an entity baed on its HomeKit characterstics metadata.""" + """Configure an entity baed on its HomeKit characteristics metadata.""" accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid @@ -124,7 +124,7 @@ class HomeKitEntity(Entity): """Collect new data from bridge and update the entity state in hass.""" accessory_state = self._accessory.current_state.get(self._aid, {}) for iid, result in accessory_state.items(): - # No value so dont process this result + # No value so don't process this result if "value" not in result: continue diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 64581da45b1..11cb607842a 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -77,7 +77,7 @@ class HKDevice: # The platorms we have forwarded the config entry so far. If a new # accessory is added to a bridge we may have to load additional # platforms. We don't want to load all platforms up front if its just - # a lightbulb. And we dont want to forward a config entry twice + # a lightbulb. And we don't want to forward a config entry twice # (triggers a Config entry already set up error) self.platforms = set() @@ -331,7 +331,7 @@ class HKDevice: key = (row["aid"], row["iid"]) # If the key was returned by put_characteristics() then the - # change didnt work + # change didn't work if key in results: continue diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ebbee1abc44..d1c3b987a73 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -305,7 +305,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Representation of the HomematicIP power comsumption value.""" + """Representation of the HomematicIP power consumption value.""" return self._device.currentPowerConsumption @property @@ -356,7 +356,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Representation of the HomematicIP todays rain value.""" + """Representation of the HomematicIP today's rain value.""" return round(self._device.todayRainCounter, 2) @property diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 9a44566bb4a..f195a458477 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -139,7 +139,7 @@ def async_register_services(hass, config, insteon_modem): def print_aldb(service): """Print the All-Link Database for a device.""" # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. + # Future direction is to create an INSTEON control panel. entity_id = service.data[CONF_ENTITY_ID] signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" dispatcher_send(hass, signal) @@ -147,7 +147,7 @@ def async_register_services(hass, config, insteon_modem): def print_im_aldb(service): """Print the All-Link Database for a device.""" # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. + # Future direction is to create an INSTEON control panel. print_aldb_to_log(insteon_modem.aldb) def x10_all_units_off(service): diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 75622c29b1c..3f193993c2b 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -279,7 +279,7 @@ class iOSIdentifyDeviceView(HomeAssistantView): name = "api:ios:identify" def __init__(self, config_path): - """Initiliaze the view.""" + """Initialize the view.""" self._config_path = config_path async def post(self, request): diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index c49144f1db9..7690600786e 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -44,11 +44,11 @@ class DiscoveryService(pizone.Listener): async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) def controller_update(self, ctrl: pizone.Controller) -> None: - """System update message is recieved from the controller.""" + """System update message is received from the controller.""" async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None: - """Zone update message is recieved from the controller.""" + """Zone update message is received from the controller.""" async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index c35a0cc00bf..ba9d52b7721 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -178,7 +178,7 @@ class VarAbs(LcnServiceCall): """Set absolute value of a variable or setpoint. Variable has to be set as counter! - Reguator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT. + Regulator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT. """ schema = LcnServiceCall.schema.extend( diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 8043469d43b..961718a30ee 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,4 +1,4 @@ -"""Support for settting the level of logging for components.""" +"""Support for setting the level of logging for components.""" from collections import OrderedDict import logging diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 28951df545a..9b4c3fc1198 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -737,7 +737,7 @@ class MediaPlayerDevice(Entity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" supported_features = self.supported_features or 0 data = {} diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 759f7f6fc89..683390429c3 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -12,15 +12,15 @@ from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME @callback def configured_instances(hass): """Return a set of configured SimpliSafe instances.""" - entites = [] + entries = [] for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("track_home"): - entites.append("home") + entries.append("home") continue - entites.append( + entries.append( f"{entry.data.get(CONF_LATITUDE)}-{entry.data.get(CONF_LONGITUDE)}" ) - return set(entites) + return set(entries) class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 18809f08d4f..b3d3e0ea285 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -113,7 +113,7 @@ class MfiSwitch(SwitchDevice): @property def device_state_attributes(self): - """Return the state attributes fof the device.""" + """Return the state attributes for the device.""" attr = {} attr["volts"] = round(self._port.data.get("v_rms", 0), 1) attr["amps"] = round(self._port.data.get("i_rms", 0), 1) diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 2243b6cc5ce..2a503651d4b 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -299,7 +299,7 @@ class MikrotikHub: @property def firmware(self): - """Return the firware of the hub.""" + """Return the firmware of the hub.""" return self._mk_data.firmware @property diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ed6144af47e..5791d17f6dd 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -91,7 +91,7 @@ async def async_setup(hass, config): """Set up the pi_hole integration.""" def get_data(): - """Retrive component data.""" + """Retrieve component data.""" return hass.data[DOMAIN] def ensure_api_token(call_data): diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 0ae62b31865..ca4eea32bd6 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,4 +1,4 @@ -"""Constants for the pi_hole intergration.""" +"""Constants for the pi_hole integration.""" from datetime import timedelta DOMAIN = "pi_hole" diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index c20296a2c18..d77cb4f56da 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -334,7 +334,7 @@ class PrometheusMetrics: @staticmethod def _sensor_fallback_metric(state, unit): - """Get metric from fallback logic for compatability.""" + """Get metric from fallback logic for compatibility.""" if unit in (None, ""): _LOGGER.debug("Unsupported sensor: %s", state.entity_id) return None diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 8df1191a420..507e3c133cc 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -233,7 +233,7 @@ class RMVDepartureData: ) except RMVtransportApiConnectionError: self.departures = [] - _LOGGER.warning("Could not retrive data from rmv.de") + _LOGGER.warning("Could not retrieve data from rmv.de") return self.station = _data.get("station") _deps = [] diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 30399640088..797780d562a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -217,7 +217,7 @@ class SAJsensor(Entity): @property def per_total_basis(self) -> bool: - """Return if the sensors value is cummulative or not.""" + """Return if the sensors value is cumulative or not.""" return self._sensor.per_total_basis @property diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 27e2fe9b563..da07290f422 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -100,7 +100,7 @@ class SigfoxAPI: @property def auth(self): - """Return the API authentification.""" + """Return the API authentication.""" return self._auth @property diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index d34653e60e7..c31ab97cb95 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -199,7 +199,7 @@ class Smappee: try: return self._smappy.get_consumption(location_id, start, end, aggregation) except RequestException as error: - _LOGGER.error("Error getting comsumption from Smappee cloud. (%s)", error) + _LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error) def get_sensor_consumption(self, location_id, sensor_id, aggregation, delta): """Update data from Smappee.""" @@ -221,7 +221,7 @@ class Smappee: location_id, sensor_id, start, end, aggregation ) except RequestException as error: - _LOGGER.error("Error getting comsumption from Smappee cloud. (%s)", error) + _LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error) def actuator_on(self, location_id, actuator_id, is_remote_switch, duration=None): """Turn on actuator.""" diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 7c8c9380522..62bf99ab383 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -54,7 +54,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return True async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 5cb2d5deec1..111155b27b6 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -54,7 +54,7 @@ class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: # set some defaults in case we need to return to the form diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 99ffd833eb3..6cd07c7f926 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -95,7 +95,7 @@ def stream_worker(hass, stream, quit_event): if packet.is_keyframe: # Calculate the segment duration by multiplying the presentation # timestamp by the time base, which gets us total seconds. - # By then dividing by the seqence, we can calculate how long + # By then dividing by the sequence, we can calculate how long # each segment is, assuming the stream starts from 0. segment_duration = (packet.pts * packet.time_base) / sequence # Save segment to outputs diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index cab57c59ac8..72507b3d148 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -126,14 +126,14 @@ class TodSensor(BinarySensorDevice): current_local_date = self.current_datetime.astimezone( self.hass.config.time_zone ).date() - # calcuate utc datetime corecponding to local time + # calculate utc datetime corecponding to local time utc_datetime = self.hass.config.time_zone.localize( datetime.combine(current_local_date, naive_time) ).astimezone(tz=pytz.UTC) return utc_datetime def _calculate_initial_boudary_time(self): - """Calculate internal absolute time boudaries.""" + """Calculate internal absolute time boundaries.""" nowutc = self.current_datetime # If after value is a sun event instead of absolute time if is_sun_event(self._after): diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 348826a1264..612561707b1 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -137,7 +137,7 @@ class ToonData: def update(self, now=None): """Update all Toon data and notify entities.""" - # Ignore the TTL meganism from client library + # Ignore the TTL mechanism from client library # It causes a lots of issues, hence we take control over caching self._toon._clear_cache() # pylint: disable=protected-access diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 0fe826be9af..40fe7b01cb0 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -246,7 +246,7 @@ class TradfriLight(TradfriBaseDevice, Light): color_command = self._device_control.set_hsb(**color_data) transition_time = None - # HSB can always be set, but color temp + brightness is bulb dependant + # HSB can always be set, but color temp + brightness is bulb dependent command = dimmer_command if command is not None: command += color_command diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 1756df7baee..3d85a76f2bd 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -80,7 +80,7 @@ class TransmissionSwitch(ToggleEntity): def turn_off(self, **kwargs): """Turn the device off.""" if self.type == "on_off": - _LOGGING.debug("Stoping all torrents") + _LOGGING.debug("Stopping all torrents") self._tm_client.api.stop_torrents() if self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission off") diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 3dab92b89f8..8c47e716b80 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -140,7 +140,7 @@ class UtilityMeterSensor(RestoreEntity): diff = Decimal(new_state.state) - Decimal(old_state.state) if (not self._sensor_net_consumption) and diff < 0: - # Source sensor just rolled over for unknow reasons, + # Source sensor just rolled over for unknown reasons, return self._state += diff diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 225a6ed72bc..3cd2de600e3 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -248,7 +248,7 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" if self.fan_speed is not None: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} @@ -330,7 +330,7 @@ class StateVacuumDevice(_BaseVacuum): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" if self.fan_speed is not None: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 9325acf0608..1d081b711a8 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -49,7 +49,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: name = slugify(user_input[CONF_NAME]) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index ecff3105ae0..4de0a58a881 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -145,7 +145,7 @@ class WaterHeaterDevice(Entity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" supported_features = self.supported_features or 0 data = { diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index 93d53159702..500f1fd3f2a 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -131,7 +131,7 @@ set_nimbus_dial_configuration: description: The minimum value allowed to be set example: 0 max_value: - description: The maximum value allowd to be set + description: The maximum value allowed to be set example: 500 min_position: description: The minimum position the dial hand can rotate to generally [0-360] @@ -141,10 +141,10 @@ set_nimbus_dial_configuration: example: 360 set_nimbus_dial_state: - description: Set the value and lables of an individual nimbus dial + description: Set the value and labels of an individual nimbus dial fields: entity_id: - description: Name fo the entity to set. + description: Name of the entity to set. example: 'wink.nimbus_dial_3' value: description: The value that should be set (Should be between min_value and max_value) diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index 592a1d1342e..c92522008be 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -2,7 +2,7 @@ enable_output: description: Enable or disable an output port fields: entity_id: - description: Name(s) of entites to enable/disable port on. + description: Name(s) of entities to enable/disable port on. example: 'media_player.yamaha' port: description: Name of port to enable/disable. diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index e5a199c5bbd..dd661835367 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -424,7 +424,7 @@ class ZHAGateway: # ZHA already has an initialized device so either the device was assigned a # new nwk or device was physically reset and added again without being removed _LOGGER.debug( - "device - %s has been reset and readded or its nwk address changed", + "device - %s has been reset and re-added or its nwk address changed", f"0x{device.nwk:04x}:{device.ieee}", ) await self._async_device_rejoined(zha_device) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 203131afdb1..62f5b9acbaf 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -88,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub_is_initialized = False async def startup(): - """Start hub socket after all climate entity is setted up.""" + """Start hub socket after all climate entity is set up.""" nonlocal hub_is_initialized if not all([device.is_initialized for device in devices]): return diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 840418fb063..4ee9b8b9cc9 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -529,7 +529,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): self._mode().data = operation_mode def turn_aux_heat_on(self): - """Turn auxillary heater on.""" + """Turn auxiliary heater on.""" if not self._aux_heat: return operation_mode = AUX_HEAT_ZWAVE_MODE @@ -537,7 +537,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): self._mode().data = operation_mode def turn_aux_heat_off(self): - """Turn auxillary heater off.""" + """Turn auxiliary heater off.""" if not self._aux_heat: return if HVAC_MODE_HEAT in self._hvac_mapping: diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index 44e73da320f..382d2c4dbf2 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -180,7 +180,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if len(str(usercode)) < 4: _LOGGER.error( "Invalid code provided: (%s) " - "usercode must be atleast 4 and at most" + "usercode must be at least 4 and at most" " %s digits", usercode, len(value.data), diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 793e8be0045..d8e64172ba0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -947,7 +947,7 @@ class SystemOptions: self.disable_new_entities = disable_new_entities def as_dict(self) -> Dict[str, Any]: - """Return dictionary version of this config entrys system options.""" + """Return dictionary version of this config entries system options.""" return {"disable_new_entities": self.disable_new_entities} diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 4dd1c7acf50..4a115762be4 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -30,7 +30,7 @@ class UnknownHandler(FlowError): class UnknownFlow(FlowError): - """Uknown flow specified.""" + """Unknown flow specified.""" class UnknownStep(FlowError): diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index 0b274458045..2e3270879f0 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -42,7 +42,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): def process( self, msg: Any, kwargs: MutableMapping[str, Any] ) -> Tuple[Any, MutableMapping[str, Any]]: - """Process the keyward args in preparation for logging.""" + """Process the keyword args in preparation for logging.""" return ( msg, { diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 8565315f87f..77642ce5052 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -469,7 +469,7 @@ def _wrap_state(hass, state): def _get_state(hass, entity_id): state = hass.states.get(entity_id) if state is None: - # Only need to collect if none, if not none collect first actuall + # Only need to collect if none, if not none collect first actual # access to the state properties in the state wrapper. _collect_state(hass, entity_id) return None diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 24cf8309228..9a5ae82d4a2 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) def is_virtual_env() -> bool: - """Return if we run in a virtual environtment.""" + """Return if we run in a virtual environment.""" # Check supports venv && virtualenv return getattr(sys, "base_prefix", sys.prefix) != sys.prefix or hasattr( sys, "real_prefix" diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 48d0a20ea73..fda5081e7c3 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -56,7 +56,7 @@ def gather_info(arguments) -> Info: YES_NO = { "validators": [["Type either 'yes' or 'no'", lambda value: value in ("yes", "no")]], - "convertor": lambda value: value == "yes", + "converter": lambda value: value == "yes", } @@ -155,8 +155,8 @@ def _gather_info(fields) -> dict: break if hint is None: - if "convertor" in info: - value = info["convertor"](value) + if "converter" in info: + value = info["converter"](value) answers[key] = value return answers diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index bc4ecaab712..c79d76baf4f 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -321,7 +321,7 @@ async def test_include_exclude_config(hass): async def test_setup_user_no_notify_service(hass): - """Test setup flow abort if there is no avilable notify service.""" + """Test setup flow abort if there is no available notify service.""" async_mock_service(hass, "notify", "test1", NOTIFY_SERVICE_SCHEMA) notify_auth_module = await auth_mfa_module_from_config( hass, {"type": "notify", "exclude": "test1"} diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 2ff75c579e5..9d93e5e7042 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -453,7 +453,7 @@ async def test_refresh_token_type_long_lived_access_token(hass): async def test_cannot_deactive_owner(mock_hass): - """Test that we cannot deactive the owner.""" + """Test that we cannot deactivate the owner.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) owner = MockUser(is_owner=True).add_to_auth_manager(manager) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index dbc08a43bfa..01c8b27bfcc 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -541,7 +541,7 @@ async def test_rendering_template_legacy_user( async def test_api_call_service_not_found(hass, mock_api_client): - """Test if the API failes 400 if unknown service.""" + """Test if the API fails 400 if unknown service.""" resp = await mock_api_client.post( const.URL_API_SERVICES_SERVICE.format("test_domain", "test_service") ) @@ -549,7 +549,7 @@ async def test_api_call_service_not_found(hass, mock_api_client): async def test_api_call_service_bad_data(hass, mock_api_client): - """Test if the API failes 400 if unknown service.""" + """Test if the API fails 400 if unknown service.""" test_value = [] @ha.callback diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index a275e57653d..8135f4e8e2c 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -95,7 +95,7 @@ async def test_apprise_notification(hass): assert await async_setup_component(hass, BASE_COMPONENT, config) await hass.async_block_till_done() - # Test the existance of our service + # Test the existence of our service assert hass.services.has_service(BASE_COMPONENT, "test") # Test the call to our underlining notify() call @@ -134,7 +134,7 @@ async def test_apprise_notification_with_target(hass, tmp_path): assert await async_setup_component(hass, BASE_COMPONENT, config) await hass.async_block_till_done() - # Test the existance of our service + # Test the existence of our service assert hass.services.has_service(BASE_COMPONENT, "test") # Test the call to our underlining notify() call diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 8448a25a7fd..2ff31a8fd4f 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -123,7 +123,7 @@ async def test_turn_off(player, state): @pytest.mark.parametrize("mute", [True, False]) async def test_mute_volume(player, state, mute): - """Test mute functionallity.""" + """Test mute functionality.""" await player.async_mute_volume(mute) state.set_mute.assert_called_with(mute) player.async_schedule_update_ha_state.assert_called_with() @@ -200,14 +200,14 @@ async def test_select_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_m async def test_volume_up(player, state): - """Test mute functionallity.""" + """Test mute functionality.""" await player.async_volume_up() state.inc_volume.assert_called_with() player.async_schedule_update_ha_state.assert_called_with() async def test_volume_down(player, state): - """Test mute functionallity.""" + """Test mute functionality.""" await player.async_volume_down() state.dec_volume.assert_called_with() player.async_schedule_update_ha_state.assert_called_with() diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index d9341bb3271..c8a23517ae1 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -149,7 +149,7 @@ class TestBayesianBinarySensor(unittest.TestCase): assert state.state == "off" def test_threshold(self): - """Test sensor on probabilty threshold limits.""" + """Test sensor on probability threshold limits.""" config = { "binary_sensor": { "name": "Test_Binary", diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index bcd1482195d..71005672fdb 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -19,7 +19,7 @@ HOST = "example.com" @pytest.fixture(name="test_connect") def mock_controller(): - """Mock a successfull _prt_in_configuration_exists.""" + """Mock a successful _prt_in_configuration_exists.""" with patch( "homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection", side_effect=lambda *_: mock_coro(True), diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 2338f0eaa1e..da20afba0b1 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -217,7 +217,7 @@ async def test_google_config_should_2fa(hass, mock_cloud_setup, mock_cloud_login async def test_set_username(hass): - """Test we set username during loggin.""" + """Test we set username during login.""" prefs = MagicMock( alexa_enabled=False, google_enabled=False, diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 267c57717f9..059cdb1f1e8 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -480,7 +480,7 @@ async def test_set_protection_value_failed(hass, client): resp = await client.post( "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Seuence"}), + data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), ) assert resp.status == 202 @@ -512,7 +512,7 @@ async def test_set_protection_value_nonexisting_node(hass, client): resp = await client.post( "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Seuence"}), + data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), ) assert resp.status == 404 @@ -532,7 +532,7 @@ async def test_set_protection_value_missing_class(hass, client): resp = await client.post( "/api/zwave/protection/17", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Seuence"}), + data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), ) assert resp.status == 404 diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index bb716ed17ec..eff06e3bf7d 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -30,7 +30,7 @@ INVALID_CONFIG_MINIMAL = { "api_key": "foo", "forecast": [1, 2], "hourly_forecast": [1, 2], - "monitored_conditions": ["sumary", "iocn", "temperature_high"], + "monitored_conditions": ["summary", "iocn", "temperature_high"], "scan_interval": timedelta(seconds=120), } } diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 0ddc429b2d9..8b2e7157256 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -610,7 +610,7 @@ async def test_close_cover(hass_hue, hue_client): async def test_set_position_cover(hass_hue, hue_client): - """Test setting postion cover .""" + """Test setting position cover .""" COVER_ID = "cover.living_room_window" # Turn the office light off first await hass_hue.services.async_call( diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index e23cc4f0982..c4c85d1cee0 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -88,7 +88,7 @@ class TestFacebook(unittest.TestCase): """Test sending a message without a target.""" mock.register_uri(requests_mock.POST, facebook.BASE_URL, status_code=200) - self.facebook.send_message(message="goin nowhere") + self.facebook.send_message(message="going nowhere") assert not mock.called @requests_mock.Mocker() diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index bbd332bb487..b535b35e182 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -91,7 +91,7 @@ class TestFritzboxClimate(unittest.TestCase): @patch.object(FritzboxThermostat, "set_hvac_mode") def test_set_temperature_operation_mode_precedence(self, mock_set_op): - """Test set_temperature for precedence of operation_mode arguement.""" + """Test set_temperature for precedence of operation_mode argument.""" self.thermostat.set_temperature(hvac_mode="heat", temperature=23.0) mock_set_op.assert_called_once_with("heat") self.thermostat._device.set_target_temperature.assert_not_called() diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 354751be0d2..31aced7f807 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -506,7 +506,7 @@ async def test_select_radio_favorite(hass, config_entry, config, controller, fav async def test_select_radio_favorite_command_error( hass, config_entry, config, controller, favorites, caplog ): - """Tests command error loged when playing favorite.""" + """Tests command error logged when playing favorite.""" await setup_platform(hass, config_entry, config) player = controller.players[1] # Test set radio preset diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index f67e0e2478d..ddca9698987 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -246,7 +246,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): - """Test battery service with mising linked_battery_sensor.""" + """Test battery service with missing linked_battery_sensor.""" entity_id = "homekit.accessory" linked_battery = "sensor.battery" hass.states.async_set(entity_id, "open") diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index 39ad429d6ef..3b0662dc16d 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -16,7 +16,7 @@ def create_security_system_service(): targ_state.value = 0 # According to the spec, a battery-level characteristic is normally - # part of a seperate service. However as the code was written (which + # part of a separate service. However as the code was written (which # predates this test) the battery level would have to be part of the lock # service as it is here. targ_state = service.add_characteristic("battery-level") diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index 3b17ad13e41..d47b77a37eb 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -16,7 +16,7 @@ def create_lock_service(): targ_state.value = 0 # According to the spec, a battery-level characteristic is normally - # part of a seperate service. However as the code was written (which + # part of a separate service. However as the code was written (which # predates this test) the battery level would have to be part of the lock # service as it is here. targ_state = service.add_characteristic("battery-level") diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index b1f6785b0a7..75cba40dafb 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -215,7 +215,7 @@ async def test_flow_link_timeout(hass): async def test_flow_link_unknown_error(hass): - """Test if a unknown error happend during the linking processes.""" + """Test if a unknown error happened during the linking processes.""" mock_bridge = get_mock_bridge(mock_create_user=CoroutineMock(side_effect=OSError),) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 67a23a61d8b..fa67bb2f8bf 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -307,7 +307,7 @@ async def test_restore_state(hass): async def test_default_value(hass): - """Test default value if none has been set via inital or restore state.""" + """Test default value if none has been set via initial or restore state.""" await async_setup_component( hass, DOMAIN, diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 25f541e9287..37dbfad4d35 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -179,7 +179,7 @@ async def test_name_exists(hass, api): async def test_connection_error(hass, conn_error): - """Test error when connection is unsuccesful.""" + """Test error when connection is unsuccessful.""" result = await hass.config_entries.flow.async_init( mikrotik.DOMAIN, context={"source": "user"} diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index bf2b19c735c..ea7e22239b2 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -16,7 +16,7 @@ async def test_setup_with_no_config(hass): async def test_successful_config_entry(hass): - """Test config entry successfull setup.""" + """Test config entry successful setup.""" entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) entry.add_to_hass(hass) mock_registry = Mock() diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 889410927e5..aa72549152e 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -50,7 +50,7 @@ async def test_user_connection_works(hass, mock_try_connection, mock_finish_setu async def test_user_connection_fails(hass, mock_try_connection, mock_finish_setup): - """Test if connnection cannot be made.""" + """Test if connection cannot be made.""" mock_try_connection.return_value = False result = await hass.config_entries.flow.async_init( diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 6320be3b772..e8da9b53a5e 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -229,7 +229,7 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): ' "name":"DiscoveryExpansionTest1 Device",' ' "mdl":"Generic",' ' "sw":"1.2.3.4",' - ' "mf":"Noone"' + ' "mf":"None"' " }" "}" ) diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 6cc727b1a1c..5a2687e4cf9 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -7,7 +7,7 @@ from tests.common import load_fixture async def test_entity_registry(hass, requests_mock): - """Tests that the devices are registed in the entity registry.""" + """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, LIGHT_DOMAIN) entity_registry = await hass.helpers.entity_registry.async_get_registry() diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index e2a86014f1c..6979fafc01d 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -7,7 +7,7 @@ from tests.common import load_fixture async def test_entity_registry(hass, requests_mock): - """Tests that the devices are registed in the entity registry.""" + """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, SWITCH_DOMAIN) entity_registry = await hass.helpers.entity_registry.async_get_registry() diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 92557f9d543..952e82c01be 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -75,7 +75,7 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: async def test_setup_plattform(hass): - """Test that setup plattform does nothing.""" + """Test that setup platform does nothing.""" assert await weather_smhi.async_setup_platform(hass, None, None) is None diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 46f40dd80ef..759639362e4 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -18,7 +18,7 @@ API_KEY = "a1b2c3d4e5f6g7h8" @pytest.fixture(name="test_api") def mock_controller(): - """Mock a successfull Solaredge API.""" + """Mock a successful Solaredge API.""" api = Mock() api.get_details.return_value = {"details": {"status": "active"}} with patch("solaredge.Solaredge", return_value=api): diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index cd05cf13185..7828290560a 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form(hass): @pytest.fixture(name="test_connect") def mock_controller(): - """Mock a successfull _host_in_configuration_exists.""" + """Mock a successful _host_in_configuration_exists.""" with patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", side_effect=lambda *_: mock_coro(True), diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 293f8d1e4cf..888f56efb29 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -47,7 +47,7 @@ async def test_hls_stream(hass, hass_client): # Stop stream, if it hasn't quit already stream.stop() - # Ensure playlist not accessable after stream ends + # Ensure playlist not accessible after stream ends fail_response = await http_client.get(parsed_url.path) assert fail_response.status == 404 @@ -84,7 +84,7 @@ async def test_stream_timeout(hass, hass_client): future = dt_util.utcnow() + timedelta(minutes=5) async_fire_time_changed(hass, future) - # Ensure playlist not accessable + # Ensure playlist not accessible fail_response = await http_client.get(parsed_url.path) assert fail_response.status == 404 diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index f4972ada2c7..6ee265de8d5 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -239,7 +239,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): async def test_discovery_already_configured(hass, mock_tellduslive): - """Test abort if alredy configured fires from discovery.""" + """Test abort if already configured fires from discovery.""" MockConfigEntry(domain="tellduslive", data={"host": "some-host"}).add_to_hass(hass) flow = init_config_flow(hass) diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index a0cccdcb18e..9980691085b 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -885,7 +885,7 @@ async def test_availability_template(hass, calls): async def test_availability_without_availability_template(hass, calls): - """Test that component is availble if there is no.""" + """Test that component is available if there is no.""" assert await setup.async_setup_component( hass, "cover", diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 03581d16c09..1da0c16d43c 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -24,7 +24,7 @@ class TestBinarySensorTod(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.latitute = 50.27583 + self.hass.config.latitude = 50.27583 self.hass.config.longitude = 18.98583 def teardown_method(self, method): diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 74137cf8a3a..a446bf914fb 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -307,7 +307,7 @@ async def test_get_controller_controller_unavailable(hass): async def test_get_controller_unknown_error(hass): - """Check that get_controller can handle unkown errors.""" + """Check that get_controller can handle unknown errors.""" with patch( "aiounifi.Controller.login", side_effect=aiounifi.AiounifiException ), pytest.raises(unifi.errors.AuthenticationRequired): diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index f123772a6ca..429e685c574 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -269,7 +269,7 @@ async def test_restoring_client(hass): async def test_dont_track_clients(hass): - """Test dont track clients config works.""" + """Test don't track clients config works.""" await setup_unifi_integration( hass, options={unifi.controller.CONF_TRACK_CLIENTS: False}, @@ -287,7 +287,7 @@ async def test_dont_track_clients(hass): async def test_dont_track_devices(hass): - """Test dont track devices config works.""" + """Test don't track devices config works.""" await setup_unifi_integration( hass, options={unifi.controller.CONF_TRACK_DEVICES: False}, @@ -305,7 +305,7 @@ async def test_dont_track_devices(hass): async def test_dont_track_wired_clients(hass): - """Test dont track wired clients config works.""" + """Test don't track wired clients config works.""" await setup_unifi_integration( hass, options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 66273e01f43..daeffb4ed1d 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -22,7 +22,7 @@ def mock_controller_assert(): @pytest.fixture(name="controller") def mock_controller(): - """Mock a successfull velbus controller.""" + """Mock a successful velbus controller.""" controller = Mock() with patch("velbus.Controller", return_value=controller): yield controller diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index fd78952bba4..f427e6d3e5a 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -72,7 +72,7 @@ def vizio_guess_device_type_fixture(): @pytest.fixture(name="vizio_cant_connect") def vizio_cant_connect_fixture(): - """Mock vizio device cant connect.""" + """Mock vizio device can't connect.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", return_value=False, diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 9805d2def46..41da5c0267b 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -316,7 +316,7 @@ async def test_zeroconf_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - # Apply discovery updates to entry to mimick when user hits submit without changing + # Apply discovery updates to entry to mimic when user hits submit without changing # defaults which were set from discovery parameters user_input = result["data_schema"](discovery_info) diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index f42f2f6af2e..f1eec4da942 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -36,7 +36,7 @@ def init_valid_settings(hass, config): @pytest.fixture(name="invalid_provider_settings") def init_invalid_provider_settings(hass, config): - """Set invalid provider data and initalize component.""" + """Set invalid provider data and initialize component.""" config["notify"][CONF_PROVIDER] = "FantasyMobile" # invalid provider return async_setup_component(hass, "notify", config) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 4c913e10034..b4fe0883bf3 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -110,7 +110,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): This will build devices for all cluster ids that exist in cluster_ids. They get added to the network and then the sensor component is loaded - which will cause sensor entites to get created for each device. + which will cause sensor entities to get created for each device. A dict containing relevant device info for testing is returned. It contains the entity id, zigpy device, and the zigbee cluster for the sensor. """ diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 631bf0a0ce8..3586c992c08 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -342,7 +342,7 @@ def test_get_device_detects_single_setpoint_device(device_single_setpoint): def test_default_hvac_modes(): - """Test wether all hvac modes are included in default_hvac_modes.""" + """Test whether all hvac modes are included in default_hvac_modes.""" for hvac_mode in HVAC_MODES: assert hvac_mode in DEFAULT_HVAC_MODES diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 540a3f96604..a8f72d2105c 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -100,7 +100,7 @@ async def test_network_key_validation(hass, mock_openzwave): async def test_erronous_network_key_fails_validation(hass, mock_openzwave): - """Test failing erronous network key validation.""" + """Test failing erroneous network key validation.""" test_values = [ ( "0x 01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, " diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 07cea74c05f..41f84e65f6c 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -263,7 +263,7 @@ async def test_extract_from_service_no_group_expand(hass): async def test_setup_dependencies_platform(hass): """Test we setup the dependencies of a platform. - We're explictely testing that we process dependencies even if a component + We're explicitly testing that we process dependencies even if a component with the same name has already been loaded. """ mock_integration( @@ -305,7 +305,7 @@ async def test_setup_entry(hass): async def test_setup_entry_platform_not_exist(hass): - """Test setup entry fails if platform doesnt exist.""" + """Test setup entry fails if platform does not exist.""" component = EntityComponent(_LOGGER, DOMAIN, hass) entry = MockConfigEntry(domain="non_existing") diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 8eea8ad004f..74105689957 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -394,7 +394,7 @@ async def test_using_prescribed_entity_id(hass): async def test_using_prescribed_entity_id_with_unique_id(hass): - """Test for ammending predefined entity ID because currently exists.""" + """Test for amending predefined entity ID because currently exists.""" component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities([MockEntity(entity_id="test_domain.world")]) @@ -839,7 +839,7 @@ async def test_override_restored_entities(hass): async def test_platform_with_no_setup(hass, caplog): - """Test setting up a platform that doesnt' support setup.""" + """Test setting up a platform that does not support setup.""" entity_platform = MockEntityPlatform( hass, domain="mock-integration", platform_name="mock-platform", platform=None ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cef8baec70e..313710d03b7 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -475,7 +475,7 @@ async def test_track_sunrise_update_location(hass): with patch("homeassistant.util.dt.utcnow", return_value=utc_now): async_track_sunrise(hass, lambda: runs.append(1)) - # Mimick sunrise + # Mimic sunrise _send_time_changed(hass, next_rising) await hass.async_block_till_done() assert len(runs) == 1 @@ -485,7 +485,7 @@ async def test_track_sunrise_update_location(hass): await hass.config.async_update(latitude=40.755931, longitude=-73.984606) await hass.async_block_till_done() - # Mimick sunrise + # Mimic sunrise _send_time_changed(hass, next_rising) await hass.async_block_till_done() # Did not increase @@ -501,7 +501,7 @@ async def test_track_sunrise_update_location(hass): break mod += 1 - # Mimick sunrise at new location + # Mimic sunrise at new location _send_time_changed(hass, next_rising) await hass.async_block_till_done() assert len(runs) == 2 diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 8d28bc73b88..55c2c67cee2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -348,7 +348,7 @@ async def test_call_context_user_not_exist(hass): async def test_call_context_target_all(hass, mock_service_platform_call, mock_entities): - """Check we only target allowed entities if targetting all.""" + """Check we only target allowed entities if targeting all.""" with patch( "homeassistant.auth.AuthManager.async_get_user", return_value=mock_coro( @@ -473,7 +473,7 @@ async def test_call_no_context_target_specific( async def test_call_with_match_all( hass, mock_service_platform_call, mock_entities, caplog ): - """Check we only target allowed entities if targetting all.""" + """Check we only target allowed entities if targeting all.""" await service.entity_service_call( hass, [Mock(entities=mock_entities)], diff --git a/tests/test_config.py b/tests/test_config.py index 1fc92ee954b..319ef841f46 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -576,7 +576,7 @@ async def test_merge(merge_log_err, hass): async def test_merge_try_falsy(merge_log_err, hass): - """Ensure we dont add falsy items like empty OrderedDict() to list.""" + """Ensure we don't add falsy items like empty OrderedDict() to list.""" packages = { "pack_falsy_to_lst": {"automation": OrderedDict()}, "pack_list2": {"light": OrderedDict()}, diff --git a/tox.ini b/tox.ini index 17253e1d1e1..7060a46b764 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,7 @@ deps = commands = python -m script.gen_requirements_all validate python -m script.hassfest validate + pre-commit run codespell {posargs: --all-files} pre-commit run flake8 {posargs: --all-files} pre-commit run bandit {posargs: --all-files} From 06c8e533233d37731679757901c5276b3a86d22a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 31 Jan 2020 13:55:06 -0500 Subject: [PATCH 031/378] bump quirks (#31355) --- 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 759cb4489fe..f7f70db590a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.13.1", - "zha-quirks==0.0.31", + "zha-quirks==0.0.32", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.13.0", "zigpy-xbee-homeassistant==0.9.0", diff --git a/requirements_all.txt b/requirements_all.txt index 957e54ab4f3..97532e06416 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ zengge==0.2 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.31 +zha-quirks==0.0.32 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b509c2dbc1..99cc98edb91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -696,7 +696,7 @@ yahooweather==0.10 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.31 +zha-quirks==0.0.32 # homeassistant.components.zha zigpy-deconz==0.7.0 From 958a867c1189a00b1afe3fbfa73ae4d9007939cf Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 31 Jan 2020 20:23:25 +0100 Subject: [PATCH 032/378] UniFi integration move to push messaging (#31086) * Rewrite UniFi integration to use push messaging * Add signalling for new clients/devices * Update list of known wireless clients when we get events of them connecting * Reconnection logic for websocket * Fix failing tests * Bump requirement to v12 * Add new tests * Update homeassistant/components/unifi/controller.py Co-Authored-By: Martin Hjelmare --- homeassistant/components/unifi/__init__.py | 4 +- homeassistant/components/unifi/controller.py | 141 +++++++++++------- .../components/unifi/device_tracker.py | 86 ++++------- homeassistant/components/unifi/manifest.json | 10 +- homeassistant/components/unifi/sensor.py | 56 +------ homeassistant/components/unifi/switch.py | 72 ++------- .../components/unifi/unifi_client.py | 65 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 62 +++----- tests/components/unifi/test_device_tracker.py | 53 +++++-- tests/components/unifi/test_init.py | 93 ++---------- tests/components/unifi/test_sensor.py | 4 +- tests/components/unifi/test_switch.py | 23 ++- 14 files changed, 296 insertions(+), 377 deletions(-) create mode 100644 homeassistant/components/unifi/unifi_client.py diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 65015b357a7..a21ae4ed508 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,7 +1,7 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -96,6 +96,8 @@ async def async_setup_entry(hass, config_entry): # sw_version=config.raw['swversion'], ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) + return True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 826491f6ba6..27a0b6a668c 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -5,9 +5,13 @@ import ssl from aiohttp import CookieJar import aiounifi +from aiounifi.controller import SIGNAL_CONNECTION_STATE +from aiounifi.events import WIRELESS_CLIENT_CONNECTED, WIRELESS_GUEST_CONNECTED +from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -40,6 +44,7 @@ from .const import ( ) from .errors import AuthenticationRequired, CannotConnect +RETRY_TIMER = 15 SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"] @@ -59,6 +64,11 @@ class UniFiController: self._site_name = None self._site_role = None + @property + def controller_id(self): + """Return the controller ID.""" + return CONTROLLER_ID.format(host=self.host, site=self.site) + @property def host(self): """Return the host of this controller.""" @@ -130,15 +140,47 @@ class UniFiController: return client.mac return None + @callback + def async_unifi_signalling_callback(self, signal, data): + """Handle messages back from UniFi library.""" + if signal == SIGNAL_CONNECTION_STATE: + + if data == STATE_DISCONNECTED and self.available: + LOGGER.error("Lost connection to UniFi") + + if (data == STATE_RUNNING and not self.available) or ( + data == STATE_DISCONNECTED and self.available + ): + self.available = data == STATE_RUNNING + async_dispatcher_send(self.hass, self.signal_reachable) + + if not self.available: + self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + + elif signal == "new_data" and data: + if "event" in data: + if data["event"].event in ( + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, + ): + self.update_wireless_clients() + else: + async_dispatcher_send(self.hass, self.signal_update) + + @property + def signal_reachable(self) -> str: + """Integration specific event to signal a change in connection status.""" + return f"unifi-reachable-{self.controller_id}" + @property def signal_update(self): """Event specific per UniFi entry to signal new data.""" - return f"unifi-update-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + return f"unifi-update-{self.controller_id}" @property def signal_options_update(self): """Event specific per UniFi entry to signal new options.""" - return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + return f"unifi-options-{self.controller_id}" def update_wireless_clients(self): """Update set of known to be wireless clients.""" @@ -156,59 +198,13 @@ class UniFiController: unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) - async def request_update(self): - """Request an update.""" - if self.progress is not None: - return await self.progress - - self.progress = self.hass.async_create_task(self.async_update()) - await self.progress - - self.progress = None - - async def async_update(self): - """Update UniFi controller information.""" - failed = False - - try: - with async_timeout.timeout(10): - await self.api.clients.update() - await self.api.devices.update() - if self.option_block_clients: - await self.api.clients_all.update() - - except aiounifi.LoginRequired: - try: - with async_timeout.timeout(5): - await self.api.login() - - except (asyncio.TimeoutError, aiounifi.AiounifiException): - failed = True - if self.available: - LOGGER.error("Unable to reach controller %s", self.host) - self.available = False - - except (asyncio.TimeoutError, aiounifi.AiounifiException): - failed = True - if self.available: - LOGGER.error("Unable to reach controller %s", self.host) - self.available = False - - if not failed and not self.available: - LOGGER.info("Reconnected to controller %s", self.host) - self.available = True - - self.update_wireless_clients() - - async_dispatcher_send(self.hass, self.signal_update) - async def async_setup(self): """Set up a UniFi controller.""" - hass = self.hass - try: self.api = await get_controller( - self.hass, **self.config_entry.data[CONF_CONTROLLER] + self.hass, + **self.config_entry.data[CONF_CONTROLLER], + async_callback=self.async_unifi_signalling_callback, ) await self.api.initialize() @@ -227,21 +223,23 @@ class UniFiController: LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False - wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] + wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() self.import_configuration() - self.config_entry.add_update_listener(self.async_options_updated) - for platform in SUPPORTED_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform ) ) + self.api.start_websocket() + + self.config_entry.add_update_listener(self.async_options_updated) + return True @staticmethod @@ -296,12 +294,38 @@ class UniFiController: self.config_entry, options=options ) + @callback + def reconnect(self) -> None: + """Prepare to reconnect UniFi session.""" + LOGGER.debug("Reconnecting to UniFi in %i", RETRY_TIMER) + self.hass.loop.create_task(self.async_reconnect()) + + async def async_reconnect(self) -> None: + """Try to reconnect UniFi session.""" + try: + with async_timeout.timeout(5): + await self.api.login() + self.api.start_websocket() + + except (asyncio.TimeoutError, aiounifi.AiounifiException): + self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + + @callback + def shutdown(self, event) -> None: + """Wrap the call to unifi.close. + + Used as an argument to EventBus.async_listen_once. + """ + self.api.stop_websocket() + async def async_reset(self): """Reset this controller to default state. Will cancel any scheduled setup retry and will unload the config entry. """ + self.api.stop_websocket() + for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform @@ -314,7 +338,9 @@ class UniFiController: return True -async def get_controller(hass, host, username, password, port, site, verify_ssl): +async def get_controller( + hass, host, username, password, port, site, verify_ssl, async_callback=None +): """Create a controller object and verify authentication.""" sslcontext = None @@ -335,6 +361,7 @@ async def get_controller(hass, host, username, password, port, site, verify_ssl) site=site, websession=session, sslcontext=sslcontext, + callback=async_callback, ) try: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 8b45a0f227b..859a37049b0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,6 +1,5 @@ """Track devices using UniFi controllers.""" import logging -from pprint import pformat from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -14,6 +13,7 @@ from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER +from .unifi_client import UniFiClient LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def update_controller(): """Update the values of the controller.""" - update_items(controller, async_add_entities, tracked) + add_entities(controller, async_add_entities, tracked) controller.listeners.append( async_dispatcher_connect(hass, controller.signal_update, update_controller) @@ -97,8 +97,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def update_items(controller, async_add_entities, tracked): - """Update tracked device state from the controller.""" +def add_entities(controller, async_add_entities, tracked): + """Add new tracker entities from the controller.""" new_tracked = [] for items, tracker_class in ( @@ -109,8 +109,6 @@ def update_items(controller, async_add_entities, tracked): for item_id in items: if item_id in tracked: - if tracked[item_id].enabled: - tracked[item_id].async_schedule_update_ha_state() continue tracked[item_id] = tracker_class(items[item_id], controller) @@ -120,16 +118,14 @@ def update_items(controller, async_add_entities, tracked): async_add_entities(new_tracked) -class UniFiClientTracker(ScannerEntity): +class UniFiClientTracker(UniFiClient, ScannerEntity): """Representation of a network client.""" def __init__(self, client, controller): """Set up tracked client.""" - self.client = client - self.controller = controller - self.is_wired = self.client.mac not in controller.wireless_clients - self.wired_bug = None + super().__init__(client, controller) + self.wired_bug = None if self.is_wired != self.client.is_wired: self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time @@ -151,26 +147,6 @@ class UniFiClientTracker(ScannerEntity): return True - async def async_added_to_hass(self): - """Client entity created.""" - LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac) - - async def async_update(self): - """Synchronize state with controller. - - Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. - """ - await self.controller.request_update() - - if self.is_wired and self.client.mac in self.controller.wireless_clients: - self.is_wired = False - - LOGGER.debug( - "Updating UniFi tracked client %s\n%s", - self.entity_id, - pformat(self.client.raw), - ) - @property def is_connected(self): """Return true if the client is connected to the network. @@ -198,26 +174,11 @@ class UniFiClientTracker(ScannerEntity): """Return the source type of the client.""" return SOURCE_TYPE_ROUTER - @property - def name(self) -> str: - """Return the name of the client.""" - return self.client.name or self.client.hostname - @property def unique_id(self) -> str: """Return a unique identifier for this client.""" return f"{self.client.mac}-{self.controller.site}" - @property - def available(self) -> bool: - """Return if controller is available.""" - return self.controller.available - - @property - def device_info(self): - """Return a client description for device registry.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} - @property def device_state_attributes(self): """Return the client state attributes.""" @@ -239,6 +200,7 @@ class UniFiDeviceTracker(ScannerEntity): """Set up tracked device.""" self.device = device self.controller = controller + self.listeners = [] @property def entity_registry_enabled_default(self): @@ -250,17 +212,26 @@ class UniFiDeviceTracker(ScannerEntity): async def async_added_to_hass(self): """Subscribe to device events.""" LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac) - - async def async_update(self): - """Synchronize state with controller.""" - await self.controller.request_update() - - LOGGER.debug( - "Updating UniFi tracked device %s\n%s", - self.entity_id, - pformat(self.device.raw), + self.device.register_callback(self.async_update_callback) + self.listeners.append( + async_dispatcher_connect( + self.hass, self.controller.signal_reachable, self.async_update_callback + ) ) + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self.device.remove_callback(self.async_update_callback) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + + @callback + def async_update_callback(self): + """Update the sensor's state.""" + LOGGER.debug("Updating UniFi tracked device %s", self.entity_id) + + self.async_schedule_update_ha_state() + @property def is_connected(self): """Return true if the device is connected to the network.""" @@ -325,3 +296,8 @@ class UniFiDeviceTracker(ScannerEntity): attributes["upgradable"] = self.device.upgradable return attributes + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e2bcd5b68a5..b4a4a5dab16 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,8 +3,12 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==11"], + "requirements": [ + "aiounifi==12" + ], "dependencies": [], - "codeowners": ["@kane610"], + "codeowners": [ + "@kane610" + ], "quality_scale": "platinum" -} +} \ No newline at end of file diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 9145fd8e00f..860ddf81d7d 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -4,11 +4,11 @@ import logging from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY +from .unifi_client import UniFiClient + LOGGER = logging.getLogger(__name__) ATTR_RECEIVING = "receiving" @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def update_controller(): """Update the values of the controller.""" - update_items(controller, async_add_entities, sensors) + add_entities(controller, async_add_entities, sensors) controller.listeners.append( async_dispatcher_connect(hass, controller.signal_update, update_controller) @@ -61,8 +61,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def update_items(controller, async_add_entities, sensors): - """Update sensors from the controller.""" +def add_entities(controller, async_add_entities, sensors): + """Add new sensor entities from the controller.""" new_sensors = [] for client_id in controller.api.clients: @@ -73,9 +73,6 @@ def update_items(controller, async_add_entities, sensors): item_id = f"{direction}-{client_id}" if item_id in sensors: - sensor = sensors[item_id] - if sensor.enabled: - sensor.async_schedule_update_ha_state() continue sensors[item_id] = sensor_class( @@ -87,14 +84,8 @@ def update_items(controller, async_add_entities, sensors): async_add_entities(new_sensors) -class UniFiBandwidthSensor(Entity): - """UniFi Bandwidth sensor base class.""" - - def __init__(self, client, controller): - """Set up client.""" - self.client = client - self.controller = controller - self.is_wired = self.client.mac not in controller.wireless_clients +class UniFiRxBandwidthSensor(UniFiClient): + """Receiving bandwidth sensor.""" @property def entity_registry_enabled_default(self): @@ -103,37 +94,6 @@ class UniFiBandwidthSensor(Entity): return True return False - async def async_added_to_hass(self): - """Client entity created.""" - LOGGER.debug("New UniFi bandwidth sensor %s (%s)", self.name, self.client.mac) - - async def async_update(self): - """Synchronize state with controller. - - Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. - """ - LOGGER.debug( - "Updating UniFi bandwidth sensor %s (%s)", self.entity_id, self.client.mac - ) - await self.controller.request_update() - - if self.is_wired and self.client.mac in self.controller.wireless_clients: - self.is_wired = False - - @property - def available(self) -> bool: - """Return if controller is available.""" - return self.controller.available - - @property - def device_info(self): - """Return a device description for device registry.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} - - -class UniFiRxBandwidthSensor(UniFiBandwidthSensor): - """Receiving bandwidth sensor.""" - @property def state(self): """Return the state of the sensor.""" @@ -153,7 +113,7 @@ class UniFiRxBandwidthSensor(UniFiBandwidthSensor): return f"rx-{self.client.mac}" -class UniFiTxBandwidthSensor(UniFiBandwidthSensor): +class UniFiTxBandwidthSensor(UniFiRxBandwidthSensor): """Transmitting bandwidth sensor.""" @property diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index b1f62131eb4..be6002e886e 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,15 +1,15 @@ """Support for devices connected to UniFi POE.""" import logging -from pprint import pformat from homeassistant.components.switch import SwitchDevice from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +from .unifi_client import UniFiClient + LOGGER = logging.getLogger(__name__) @@ -20,7 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for UniFi component. - Switches are controlling network switch ports with Poe. + Switches are controlling network access and switch ports with POE. """ controller = get_controller_from_config_entry(hass, config_entry) @@ -55,7 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def update_controller(): """Update the values of the controller.""" - update_items(controller, async_add_entities, switches, switches_off) + add_entities(controller, async_add_entities, switches, switches_off) controller.listeners.append( async_dispatcher_connect(hass, controller.signal_update, update_controller) @@ -66,8 +66,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def update_items(controller, async_add_entities, switches, switches_off): - """Update POE port state from the controller.""" +def add_entities(controller, async_add_entities, switches, switches_off): + """Add new switch entities from the controller.""" new_switches = [] devices = controller.api.devices @@ -77,13 +77,6 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - if switches[block_client_id].enabled: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -99,13 +92,6 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - if switches[poe_client_id].enabled: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -148,42 +134,21 @@ def update_items(controller, async_add_entities, switches, switches_off): async_add_entities(new_switches) -class UniFiClient: - """Base class for UniFi switches.""" - - def __init__(self, client, controller): - """Set up switch.""" - self.client = client - self.controller = controller - - async def async_update(self): - """Synchronize state with controller.""" - await self.controller.request_update() - - @property - def name(self): - """Return the name of the client.""" - return self.client.name or self.client.hostname - - @property - def device_info(self): - """Return a device description for device registry.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} - - class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): """Representation of a client that uses POE.""" def __init__(self, client, controller): """Set up POE switch.""" super().__init__(client, controller) + self.poe_mode = None if self.client.sw_port and self.port.poe_mode != "off": self.poe_mode = self.port.poe_mode async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) + await super().async_added_to_hass() + state = await self.async_get_last_state() if state is None: @@ -198,16 +163,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] - async def async_update(self): - """Log client information after update.""" - await super().async_update() - - LOGGER.debug( - "Updating UniFi POE controlled client %s\n%s", - self.entity_id, - pformat(self.client.raw), - ) - @property def unique_id(self): """Return a unique identifier for this switch.""" @@ -267,10 +222,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" - async def async_added_to_hass(self): - """Call when entity about to be added to Home Assistant.""" - LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) - @property def unique_id(self): """Return a unique identifier for this switch.""" @@ -281,11 +232,6 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Return true if client is allowed to connect.""" return not self.client.blocked - @property - def available(self): - """Return if controller is available.""" - return self.controller.available - async def async_turn_on(self, **kwargs): """Turn on connectivity for client.""" await self.controller.api.clients.async_unblock(self.client.mac) diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py new file mode 100644 index 00000000000..2e18f55a57b --- /dev/null +++ b/homeassistant/components/unifi/unifi_client.py @@ -0,0 +1,65 @@ +"""Base class for UniFi clients.""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +LOGGER = logging.getLogger(__name__) + + +class UniFiClient(Entity): + """Base class for UniFi clients.""" + + def __init__(self, client, controller) -> None: + """Set up client.""" + self.client = client + self.controller = controller + self.listeners = [] + self.is_wired = self.client.mac not in controller.wireless_clients + + async def async_added_to_hass(self) -> None: + """Client entity created.""" + LOGGER.debug("New UniFi client %s (%s)", self.name, self.client.mac) + self.client.register_callback(self.async_update_callback) + self.listeners.append( + async_dispatcher_connect( + self.hass, self.controller.signal_reachable, self.async_update_callback + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect client object when removed.""" + self.client.remove_callback(self.async_update_callback) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + + @callback + def async_update_callback(self) -> None: + """Update the clients state.""" + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + LOGGER.debug("Updating client %s %s", self.entity_id, self.client.mac) + self.async_schedule_update_ha_state() + + @property + def name(self) -> str: + """Return the name of the client.""" + return self.client.name or self.client.hostname + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.controller.available + + @property + def device_info(self) -> dict: + """Return a client description for device registry.""" + return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False diff --git a/requirements_all.txt b/requirements_all.txt index 97532e06416..6997ea10869 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ aiopylgtv==0.3.2 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==11 +aiounifi==12 # homeassistant.components.wwlln aiowwlln==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99cc98edb91..62a755f5145 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ aiopylgtv==0.3.2 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==11 +aiounifi==12 # homeassistant.components.wwlln aiowwlln==2.0.2 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index a446bf914fb..e1b2b2355c4 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -111,9 +111,12 @@ async def setup_unifi_integration( return mock_client_all_responses.popleft() return {} + # "aiounifi.Controller.start_websocket", return_value=True with patch("aiounifi.Controller.login", return_value=True), patch( "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): + ), patch("aiounifi.Controller.request", new=mock_request), patch.object( + aiounifi.websocket.WSClient, "start", return_value=True + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -233,47 +236,28 @@ async def test_reset_after_successful_setup(hass): assert len(controller.listeners) == 0 -async def test_failed_update_failed_login(hass): - """Running update can handle a failed login.""" +async def test_wireless_client_event_calls_update_wireless_devices(hass): + """Call update_wireless_devices method when receiving wireless client event.""" controller = await setup_unifi_integration(hass) - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.LoginRequired - ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): - await controller.async_update() - await hass.async_block_till_done() + with patch( + "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients", + return_value=None, + ) as wireless_clients_mock: + controller.api.websocket._data = { + "meta": {"rc": "ok", "message": "events"}, + "data": [ + { + "datetime": "2020-01-20T19:37:04Z", + "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED, + "msg": "User[11:22:33:44:55:66] has connected to WLAN", + "time": 1579549024893, + } + ], + } + controller.api.session_handler("data") - assert controller.available is False - - -async def test_failed_update_successful_login(hass): - """Running update can login when requested.""" - controller = await setup_unifi_integration(hass) - - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.LoginRequired - ), patch.object(controller.api, "login", return_value=Mock(True)): - await controller.async_update() - await hass.async_block_till_done() - - assert controller.available is True - - -async def test_failed_update(hass): - """Running update can login when requested.""" - controller = await setup_unifi_integration(hass) - - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.AiounifiException - ): - await controller.async_update() - await hass.async_block_till_done() - - assert controller.available is False - - await controller.async_update() - await hass.async_block_till_done() - assert controller.available is True + assert wireless_clients_mock.assert_called_once async def test_get_controller(hass): diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 429e685c574..86595fe43e2 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,6 +2,8 @@ from copy import copy from datetime import timedelta +from aiounifi.controller import SIGNAL_CONNECTION_STATE +from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING from asynctest import patch from homeassistant import config_entries @@ -136,11 +138,12 @@ async def test_tracked_devices(hass): client_1_copy = copy(CLIENT_1) client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + event = {"meta": {"message": "sta:sync"}, "data": [client_1_copy]} + controller.api.message_handler(event) device_1_copy = copy(DEVICE_1) device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller.mock_client_responses.append([client_1_copy]) - controller.mock_device_responses.append([device_1_copy]) - await controller.async_update() + event = {"meta": {"message": "device:sync"}, "data": [device_1_copy]} + controller.api.message_handler(event) await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -149,16 +152,39 @@ async def test_tracked_devices(hass): device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" + # Controller unavailable + controller.async_unifi_signalling_callback( + SIGNAL_CONNECTION_STATE, STATE_DISCONNECTED + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == STATE_UNAVAILABLE + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == STATE_UNAVAILABLE + + # Controller available + controller.async_unifi_signalling_callback(SIGNAL_CONNECTION_STATE, STATE_RUNNING) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "home" + + # Disabled device is unavailable device_1_copy = copy(DEVICE_1) device_1_copy["disabled"] = True - controller.mock_client_responses.append({}) - controller.mock_device_responses.append([device_1_copy]) - await controller.async_update() + event = {"meta": {"message": "device:sync"}, "data": [device_1_copy]} + controller.api.message_handler(event) await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE + # Don't track wired clients nor devices controller.config_entry.add_update_listener(controller.async_options_updated) hass.config_entries.async_update_entry( controller.config_entry, @@ -194,9 +220,8 @@ async def test_wireless_client_go_wired_issue(hass): client_1_client["is_wired"] = True client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller.mock_client_responses.append([client_1_client]) - controller.mock_device_responses.append({}) - await controller.async_update() + event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + controller.api.message_handler(event) await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -207,9 +232,8 @@ async def test_wireless_client_go_wired_issue(hass): "utcnow", return_value=(dt_util.utcnow() + timedelta(minutes=5)), ): - controller.mock_client_responses.append([client_1_client]) - controller.mock_device_responses.append({}) - await controller.async_update() + event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + controller.api.message_handler(event) await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -217,9 +241,8 @@ async def test_wireless_client_go_wired_issue(hass): client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller.mock_client_responses.append([client_1_client]) - controller.mock_device_responses.append({}) - await controller.async_update() + event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + controller.api.message_handler(event) await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 1f5a3852e16..12f9c1bfd17 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -4,6 +4,8 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi from homeassistant.setup import async_setup_component +from .test_controller import setup_unifi_integration + from tests.common import MockConfigEntry, mock_coro @@ -42,67 +44,15 @@ async def test_setup_with_config(hass): async def test_successful_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" - entry = MockConfigEntry( - domain=unifi.DOMAIN, - data={ - "controller": { - "host": "0.0.0.0", - "username": "user", - "password": "pass", - "port": 80, - "site": "default", - "verify_ssl": True, - }, - "poe_control": True, - }, - ) - entry.add_to_hass(hass) - mock_registry = Mock() - with patch.object(unifi, "UniFiController") as mock_controller, patch( - "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_coro(mock_registry), - ): - mock_controller.return_value.async_setup.return_value = mock_coro(True) - mock_controller.return_value.mac = "00:11:22:33:44:55" - assert await unifi.async_setup_entry(hass, entry) is True - - assert len(mock_controller.mock_calls) == 2 - p_hass, p_entry = mock_controller.mock_calls[0][1] - - assert p_hass is hass - assert p_entry is entry - - assert len(mock_registry.mock_calls) == 1 - assert mock_registry.mock_calls[0][2] == { - "config_entry_id": entry.entry_id, - "connections": {("mac", "00:11:22:33:44:55")}, - "manufacturer": unifi.ATTR_MANUFACTURER, - "model": "UniFi Controller", - "name": "UniFi Controller", - } + await setup_unifi_integration(hass) + assert hass.data[unifi.DOMAIN] async def test_controller_fail_setup(hass): """Test that a failed setup still stores controller.""" - entry = MockConfigEntry( - domain=unifi.DOMAIN, - data={ - "controller": { - "host": "0.0.0.0", - "username": "user", - "password": "pass", - "port": 80, - "site": "default", - "verify_ssl": True, - }, - "poe_control": True, - }, - ) - entry.add_to_hass(hass) - with patch.object(unifi, "UniFiController") as mock_cntrlr: mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) - assert await unifi.async_setup_entry(hass, entry) is False + await setup_unifi_integration(hass) assert hass.data[unifi.DOMAIN] == {} @@ -140,33 +90,8 @@ async def test_controller_no_mac(hass): async def test_unload_entry(hass): """Test being able to unload an entry.""" - entry = MockConfigEntry( - domain=unifi.DOMAIN, - data={ - "controller": { - "host": "0.0.0.0", - "username": "user", - "password": "pass", - "port": 80, - "site": "default", - "verify_ssl": True, - }, - "poe_control": True, - }, - ) - entry.add_to_hass(hass) + controller = await setup_unifi_integration(hass) + assert hass.data[unifi.DOMAIN] - with patch.object(unifi, "UniFiController") as mock_controller, patch( - "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_coro(Mock()), - ): - mock_controller.return_value.async_setup.return_value = mock_coro(True) - mock_controller.return_value.mac = "00:11:22:33:44:55" - assert await unifi.async_setup_entry(hass, entry) is True - - assert len(mock_controller.return_value.mock_calls) == 1 - - mock_controller.return_value.async_reset.return_value = mock_coro(True) - assert await unifi.async_unload_entry(hass, entry) - assert len(mock_controller.return_value.async_reset.mock_calls) == 1 - assert hass.data[unifi.DOMAIN] == {} + assert await unifi.async_unload_entry(hass, controller.config_entry) + assert not hass.data[unifi.DOMAIN] diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 723d6871636..668b7a36ada 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -90,8 +90,8 @@ async def test_sensors(hass): clients[1]["rx_bytes"] = 2345000000 clients[1]["tx_bytes"] = 6789000000 - controller.mock_client_responses.append(clients) - await controller.async_update() + event = {"meta": {"message": "sta:sync"}, "data": clients} + controller.api.message_handler(event) await hass.async_block_till_done() wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index cc4c41bcbfd..cd3d8785399 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -300,13 +300,17 @@ async def test_new_client_discovered_on_block_control(hass): assert len(controller.mock_requests) == 3 assert len(hass.states.async_all()) == 2 - controller.mock_client_all_responses.append([BLOCKED]) + controller.api.websocket._data = { + "meta": {"message": "sta:sync"}, + "data": [BLOCKED], + } + controller.api.session_handler("data") # Calling a service will trigger the updates to run await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 7 + assert len(controller.mock_requests) == 4 assert len(hass.states.async_all()) == 2 assert controller.mock_requests[3] == { "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, @@ -317,8 +321,8 @@ async def test_new_client_discovered_on_block_control(hass): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 11 - assert controller.mock_requests[7] == { + assert len(controller.mock_requests) == 5 + assert controller.mock_requests[4] == { "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, "method": "post", "path": "s/{site}/cmd/stamgr/", @@ -340,14 +344,17 @@ async def test_new_client_discovered_on_poe_control(hass): assert len(controller.mock_requests) == 3 assert len(hass.states.async_all()) == 2 - controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - controller.mock_device_responses.append([DEVICE_1]) + controller.api.websocket._data = { + "meta": {"message": "sta:sync"}, + "data": [CLIENT_2], + } + controller.api.session_handler("data") # Calling a service will trigger the updates to run await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 6 + assert len(controller.mock_requests) == 4 assert len(hass.states.async_all()) == 3 assert controller.mock_requests[3] == { "json": { @@ -360,7 +367,7 @@ async def test_new_client_discovered_on_poe_control(hass): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 9 + assert len(controller.mock_requests) == 5 assert controller.mock_requests[3] == { "json": { "port_overrides": [ From 44f0728c607f8a28d5658ef71b2f11f7f6ec8fad Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 31 Jan 2020 20:23:51 +0100 Subject: [PATCH 033/378] Axis - Use core to start component tests (#31328) * Fix test according to Martins comment in 31286 * Use core features rather than integration specific * Fix populate options test --- tests/components/axis/test_init.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 643ddf58ac0..cf5a3b2785a 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components import axis from homeassistant.setup import async_setup_component -from .test_device import ENTRY_CONFIG, MAC, setup_axis_integration +from .test_device import MAC, setup_axis_integration from tests.common import MockConfigEntry, mock_coro @@ -37,9 +37,10 @@ async def test_setup_entry(hass): async def test_setup_entry_fails(hass): """Test successful setup of entry.""" - entry = MockConfigEntry( - domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, options=True + config_entry = MockConfigEntry( + domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, options=True, version=2 ) + config_entry.add_to_hass(hass) mock_device = Mock() mock_device.async_setup.return_value = mock_coro(False) @@ -47,7 +48,7 @@ async def test_setup_entry_fails(hass): with patch.object(axis, "AxisNetworkDevice") as mock_device_class: mock_device_class.return_value = mock_device - assert not await axis.async_setup_entry(hass, entry) + assert not await hass.config_entries.async_setup(config_entry.entry_id) assert not hass.data[axis.DOMAIN] @@ -57,20 +58,15 @@ async def test_unload_entry(hass): device = await setup_axis_integration(hass) assert hass.data[axis.DOMAIN] - assert await axis.async_unload_entry(hass, device.config_entry) + assert await hass.config_entries.async_unload(device.config_entry.entry_id) assert not hass.data[axis.DOMAIN] async def test_populate_options(hass): """Test successful populate options.""" - entry = MockConfigEntry(domain=axis.DOMAIN, data=ENTRY_CONFIG) - entry.add_to_hass(hass) + device = await setup_axis_integration(hass, options=None) - with patch.object(axis, "get_device", return_value=mock_coro(Mock())): - - await axis.async_populate_options(hass, entry) - - assert entry.options == { + assert device.config_entry.options == { axis.CONF_CAMERA: True, axis.CONF_EVENTS: True, axis.CONF_TRIGGER_TIME: axis.DEFAULT_TRIGGER_TIME, @@ -95,7 +91,7 @@ async def test_migrate_entry(hass): assert entry.data == legacy_config assert entry.version == 1 - await axis.async_migrate_entry(hass, entry) + await entry.async_migrate(hass) assert entry.data == { axis.CONF_DEVICE: { From 06efe3a2f635efa4b0c84438f58cd4bdebbcfe3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 14:01:25 -0800 Subject: [PATCH 034/378] Fix wemo device types for lights (#31360) --- homeassistant/components/wemo/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index a615b3f5dfd..7d2cf9afc43 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -114,7 +114,7 @@ class WemoLight(Light): return { "name": self.wemo.name, "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, - "model": self.wemo.model_name, + "model": self.wemo.device_type, "manufacturer": "Belkin", } From fa2e409abdda0d5b5d7014afbf5c5f85d6dfe4ae Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 31 Jan 2020 17:14:43 -0500 Subject: [PATCH 035/378] Protect for unknown state attributes. (#31354) --- .../components/alexa/capabilities.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 8b93b911fc4..02ebdf785cd 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1326,14 +1326,20 @@ class AlexaRangeController(AlexaCapability): if name != "rangeValue": raise UnsupportedProperty(name) + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] - speed = self.entity.attributes[fan.ATTR_SPEED] - speed_index = next( - (i for i, v in enumerate(speed_list) if v == speed), None - ) - return speed_index + speed_list = self.entity.attributes.get(fan.ATTR_SPEED_LIST) + speed = self.entity.attributes.get(fan.ATTR_SPEED) + if speed_list is not None and speed is not None: + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": @@ -1349,12 +1355,13 @@ class AlexaRangeController(AlexaCapability): # Vacuum Fan Speed if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": - speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] - speed = self.entity.attributes[vacuum.ATTR_FAN_SPEED] - speed_index = next( - (i for i, v in enumerate(speed_list) if v == speed), None - ) - return speed_index + speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) + speed = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED) + if speed_list is not None and speed is not None: + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index return None From d225fc08fe8c57f2128aebec8b582302bace73df Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 31 Jan 2020 23:20:06 +0100 Subject: [PATCH 036/378] drop fritzdect (#31359) --- .coveragerc | 1 - .../components/fritzdect/__init__.py | 1 - .../components/fritzdect/manifest.json | 8 - homeassistant/components/fritzdect/switch.py | 224 ------------------ requirements_all.txt | 3 - 5 files changed, 237 deletions(-) delete mode 100644 homeassistant/components/fritzdect/__init__.py delete mode 100644 homeassistant/components/fritzdect/manifest.json delete mode 100644 homeassistant/components/fritzdect/switch.py diff --git a/.coveragerc b/.coveragerc index 39a46d97616..892a9f9de9d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -248,7 +248,6 @@ omit = homeassistant/components/fritzbox/* homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_netmonitor/sensor.py - homeassistant/components/fritzdect/switch.py homeassistant/components/fronius/sensor.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py diff --git a/homeassistant/components/fritzdect/__init__.py b/homeassistant/components/fritzdect/__init__.py deleted file mode 100644 index d64990bc3f0..00000000000 --- a/homeassistant/components/fritzdect/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The fritzdect component.""" diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json deleted file mode 100644 index 9fc91293608..00000000000 --- a/homeassistant/components/fritzdect/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "fritzdect", - "name": "AVM FRITZ!DECT", - "documentation": "https://www.home-assistant.io/integrations/fritzdect", - "requirements": ["fritzhome==1.0.4"], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py deleted file mode 100644 index f67c84ae552..00000000000 --- a/homeassistant/components/fritzdect/switch.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Support for FRITZ!DECT Switches.""" -import logging - -from fritzhome.fritz import FritzBox -from requests.exceptions import HTTPError, RequestException -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - TEMP_CELSIUS, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -# Standard Fritz Box IP -DEFAULT_HOST = "fritz.box" - -ATTR_CURRENT_CONSUMPTION = "current_consumption" -ATTR_CURRENT_CONSUMPTION_UNIT = "current_consumption_unit" -ATTR_CURRENT_CONSUMPTION_UNIT_VALUE = POWER_WATT - -ATTR_TOTAL_CONSUMPTION = "total_consumption" -ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" -ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR - -ATTR_TEMPERATURE_UNIT = "temperature_unit" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Add all switches connected to Fritz Box.""" - - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - # Log into Fritz Box - fritz = FritzBox(host, username, password) - try: - fritz.login() - except Exception: # pylint: disable=broad-except - _LOGGER.error("Login to Fritz!Box failed") - return - - # Add all actors to hass - for actor in fritz.get_actors(): - # Only add devices that support switching - if actor.has_switch: - data = FritzDectSwitchData(fritz, actor.actor_id) - data.is_online = True - add_entities([FritzDectSwitch(hass, data, actor.name)], True) - - -class FritzDectSwitch(SwitchDevice): - """Representation of a FRITZ!DECT switch.""" - - def __init__(self, hass, data, name): - """Initialize the switch.""" - self.units = hass.config.units - self.data = data - self._name = name - - @property - def name(self): - """Return the name of the FRITZ!DECT switch, if any.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attrs = {} - - if ( - self.data.has_powermeter - and self.data.current_consumption is not None - and self.data.total_consumption is not None - ): - attrs[ATTR_CURRENT_CONSUMPTION] = "{:.1f}".format( - self.data.current_consumption - ) - attrs[ATTR_CURRENT_CONSUMPTION_UNIT] = "{}".format( - ATTR_CURRENT_CONSUMPTION_UNIT_VALUE - ) - attrs[ATTR_TOTAL_CONSUMPTION] = f"{self.data.total_consumption:.3f}" - attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = "{}".format( - ATTR_TOTAL_CONSUMPTION_UNIT_VALUE - ) - - if self.data.has_temperature and self.data.temperature is not None: - attrs[ATTR_TEMPERATURE] = "{}".format( - self.units.temperature(self.data.temperature, TEMP_CELSIUS) - ) - attrs[ATTR_TEMPERATURE_UNIT] = f"{self.units.temperature_unit}" - return attrs - - @property - def current_power_w(self): - """Return the current power usage in Watt.""" - try: - return float(self.data.current_consumption) - except ValueError: - return None - - @property - def is_on(self): - """Return true if switch is on.""" - return self.data.state - - def turn_on(self, **kwargs): - """Turn the switch on.""" - if not self.data.is_online: - _LOGGER.error("turn_on: Not online skipping request") - return - - try: - actor = self.data.fritz.get_actor_by_ain(self.data.ain) - actor.switch_on() - except (RequestException, HTTPError): - _LOGGER.error("Fritz!Box query failed, triggering relogin") - self.data.is_online = False - - def turn_off(self, **kwargs): - """Turn the switch off.""" - if not self.data.is_online: - _LOGGER.error("turn_off: Not online skipping request") - return - - try: - actor = self.data.fritz.get_actor_by_ain(self.data.ain) - actor.switch_off() - except (RequestException, HTTPError): - _LOGGER.error("Fritz!Box query failed, triggering relogin") - self.data.is_online = False - - def update(self): - """Get the latest data from the fritz box and updates the states.""" - if not self.data.is_online: - _LOGGER.error("update: Not online, logging back in") - - try: - self.data.fritz.login() - except Exception: # pylint: disable=broad-except - _LOGGER.error("Login to Fritz!Box failed") - return - - self.data.is_online = True - - try: - self.data.update() - except Exception: # pylint: disable=broad-except - _LOGGER.error("Fritz!Box query failed, triggering relogin") - self.data.is_online = False - - -class FritzDectSwitchData: - """Get the latest data from the fritz box.""" - - def __init__(self, fritz, ain): - """Initialize the data object.""" - self.fritz = fritz - self.ain = ain - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - self.has_switch = False - self.has_temperature = False - self.has_powermeter = False - self.is_online = False - - def update(self): - """Get the latest data from the fritz box.""" - if not self.is_online: - _LOGGER.error("Not online skipping request") - return - - try: - actor = self.fritz.get_actor_by_ain(self.ain) - except (RequestException, HTTPError): - _LOGGER.error("Request to actor registry failed") - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - raise Exception("Request to actor registry failed") - - if actor is None: - _LOGGER.error("Actor could not be found") - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - raise Exception("Actor could not be found") - - try: - self.state = actor.get_state() - self.current_consumption = (actor.get_power() or 0.0) / 1000 - self.total_consumption = (actor.get_energy() or 0.0) / 1000 - except (RequestException, HTTPError): - _LOGGER.error("Request to actor failed") - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - raise Exception("Request to actor failed") - - self.temperature = actor.temperature - self.has_switch = actor.has_switch - self.has_temperature = actor.has_temperature - self.has_powermeter = actor.has_powermeter diff --git a/requirements_all.txt b/requirements_all.txt index 6997ea10869..8a43c7c8277 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -548,9 +548,6 @@ freesms==0.1.2 # homeassistant.components.fritzbox_netmonitor fritzconnection==1.2.0 -# homeassistant.components.fritzdect -fritzhome==1.0.4 - # homeassistant.components.google_translate gTTS-token==1.1.3 From ae76b5be5a9d2b039abb733a9730f3c2c2bb24f2 Mon Sep 17 00:00:00 2001 From: Malachi Soord Date: Fri, 31 Jan 2020 23:23:42 +0100 Subject: [PATCH 037/378] Let core resolve entity_id for lastfm from username (#31280) * Sluggify user * Simplify * Remove unused import --- homeassistant/components/lastfm/sensor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 3a830b9f4e6..1a5b7a56e8e 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -58,7 +58,6 @@ class LastfmSensor(Entity): self._unique_id = hashlib.sha256(user.encode("utf-8")).hexdigest() self._user = lastfm_api.get_user(user) self._name = user - self._entity_id = user self._lastfm = lastfm_api self._state = "Not Scrobbling" self._playcount = None @@ -76,11 +75,6 @@ class LastfmSensor(Entity): """Return the name of the sensor.""" return self._name - @property - def entity_id(self): - """Return the entity ID.""" - return f"sensor.lastfm_{self._entity_id}" - @property def state(self): """Return the state of the sensor.""" From 166d770ddda047013fb5f3d060676b652981c916 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 14:47:40 -0800 Subject: [PATCH 038/378] Update Hue data fetching (#31338) * Refactor Hue Lights to use DataCoordinator * Redo how Hue updates data * Address comments * Inherit from Entity and remove pylint disable * Add tests for debounce --- homeassistant/components/hue/__init__.py | 4 +- homeassistant/components/hue/binary_sensor.py | 35 ++- homeassistant/components/hue/bridge.py | 9 + homeassistant/components/hue/const.py | 4 + homeassistant/components/hue/helpers.py | 8 +- homeassistant/components/hue/light.py | 277 +++++++----------- homeassistant/components/hue/sensor.py | 42 +-- homeassistant/components/hue/sensor_base.py | 178 +++++------ homeassistant/helpers/debounce.py | 77 +++++ homeassistant/helpers/event.py | 2 +- homeassistant/helpers/update_coordinator.py | 135 +++++++++ tests/components/hue/conftest.py | 11 + tests/components/hue/test_light.py | 30 +- tests/components/hue/test_sensor_base.py | 30 +- tests/helpers/test_debounce.py | 62 ++++ 15 files changed, 549 insertions(+), 355 deletions(-) create mode 100644 homeassistant/helpers/debounce.py create mode 100644 homeassistant/helpers/update_coordinator.py create mode 100644 tests/components/hue/conftest.py create mode 100644 tests/helpers/test_debounce.py diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7349f4fe6a6..c8864e97607 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -122,7 +122,7 @@ async def async_setup_entry( if not await bridge.async_setup(): return False - hass.data[DOMAIN][host] = bridge + hass.data[DOMAIN][entry.entry_id] = bridge config = bridge.api.config # For backwards compat @@ -151,5 +151,5 @@ async def async_setup_entry( async def async_unload_entry(hass, entry): """Unload a config entry.""" - bridge = hass.data[DOMAIN].pop(entry.data["host"]) + bridge = hass.data[DOMAIN].pop(entry.entry_id) return await bridge.async_reset() diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index e4b7dd85e37..319f8f5fa19 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -6,27 +6,18 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, BinarySensorDevice, ) -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - SensorManager, - async_setup_entry as shared_async_setup_entry, -) + +from .const import DOMAIN as HUE_DOMAIN +from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" - SensorManager.sensor_config_map.update( - { - TYPE_ZLL_PRESENCE: { - "binary": True, - "name_format": PRESENCE_NAME_FORMAT, - "class": HuePresence, - } - } - ) - await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=True) + await hass.data[HUE_DOMAIN][ + config_entry.entry_id + ].sensor_manager.async_register_component(True, async_add_entities) class HuePresence(GenericZLLSensor, BinarySensorDevice): @@ -34,9 +25,6 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): device_class = DEVICE_CLASS_MOTION - async def _async_update_ha_state(self, *args, **kwargs): - await self.async_update_ha_state(self, *args, **kwargs) - @property def is_on(self): """Return true if the binary sensor is on.""" @@ -51,3 +39,14 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): if "sensitivitymax" in self.sensor.config: attributes["sensitivity_max"] = self.sensor.config["sensitivitymax"] return attributes + + +SENSOR_CONFIG_MAP.update( + { + TYPE_ZLL_PRESENCE: { + "binary": True, + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + } + } +) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 58a744dd5b0..a153ed7a096 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -13,6 +13,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER from .errors import AuthenticationRequired, CannotConnect from .helpers import create_config_flow +from .sensor_base import SensorManager SERVICE_HUE_SCENE = "hue_activate_scene" ATTR_GROUP_NAME = "group_name" @@ -35,6 +36,9 @@ class HueBridge: self.authorized = False self.api = None self.parallel_updates_semaphore = None + # Jobs to be executed when API is reset. + self.reset_jobs = [] + self.sensor_manager = None @property def host(self): @@ -72,6 +76,7 @@ class HueBridge: return False self.api = bridge + self.sensor_manager = SensorManager(self) hass.async_create_task( hass.config_entries.async_forward_entry_setup(self.config_entry, "light") @@ -118,6 +123,9 @@ class HueBridge: self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) + while self.reset_jobs: + self.reset_jobs.pop()() + # If setup was successful, we set api variable, forwarded entry and # register service results = await asyncio.gather( @@ -131,6 +139,7 @@ class HueBridge: self.config_entry, "sensor" ), ) + # None and True are OK return False not in results diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index d884389c0c1..e48cd4a8583 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -4,3 +4,7 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "hue" API_NUPNP = "https://www.meethue.com/api/nupnp" + +# How long to wait to actually do the refresh after requesting it. +# We wait some time so if we control multiple lights, we batch requests. +REQUEST_REFRESH_DELAY = 0.3 diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 8a5fa973e4f..885677dc269 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -6,7 +6,7 @@ from homeassistant.helpers.entity_registry import async_get_registry as get_ent_ from .const import DOMAIN -async def remove_devices(hass, config_entry, api_ids, current): +async def remove_devices(bridge, api_ids, current): """Get items that are removed from api.""" removed_items = [] @@ -18,16 +18,16 @@ async def remove_devices(hass, config_entry, api_ids, current): entity = current[item_id] removed_items.append(item_id) await entity.async_remove() - ent_registry = await get_ent_reg(hass) + ent_registry = await get_ent_reg(bridge.hass) if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) - dev_registry = await get_dev_reg(hass) + dev_registry = await get_dev_reg(bridge.hass) device = dev_registry.async_get_device( identifiers={(DOMAIN, entity.device_id)}, connections=set() ) if device is not None: dev_registry.async_update_device( - device.id, remove_config_entry_id=config_entry.entry_id + device.id, remove_config_entry_id=bridge.config_entry.entry_id ) for item_id in removed_items: diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 2a668779cb5..7ed2dcc84f2 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,14 +1,13 @@ """Support for the Philips Hue lights.""" import asyncio from datetime import timedelta +from functools import partial import logging import random -from time import monotonic import aiohue import async_timeout -from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -28,8 +27,13 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) +from homeassistant.core import callback +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import color +from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) @@ -70,9 +74,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Hue lights from a config entry.""" - bridge = hass.data[hue.DOMAIN][config_entry.data["host"]] - cur_lights = {} - cur_groups = {} + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + light_coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + "light", + partial(async_safe_fetch, bridge, bridge.api.lights.update), + SCAN_INTERVAL, + Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + ) + + # First do a refresh to see if we can reach the hub. + # Otherwise we will declare not ready. + await light_coordinator.async_refresh() + + if light_coordinator.failed_last_update: + raise PlatformNotReady + + update_lights = partial( + async_update_items, + bridge, + bridge.api.lights, + {}, + async_add_entities, + partial(HueLight, light_coordinator, bridge, False), + ) + + # We add a listener after fetching the data, so manually trigger listener + light_coordinator.async_add_listener(update_lights) + update_lights() + + bridge.reset_jobs.append( + lambda: light_coordinator.async_remove_listener(update_lights) + ) api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) @@ -81,168 +116,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.warning("Please update your Hue bridge to support groups") allow_groups = False - # Hue updates all lights via a single API call. - # - # If we call a service to update 2 lights, we only want the API to be - # called once. - # - # The throttle decorator will return right away if a call is currently - # in progress. This means that if we are updating 2 lights, the first one - # is in the update method, the second one will skip it and assume the - # update went through and updates it's data, not good! - # - # The current mechanism will make sure that all lights will wait till - # the update call is done before writing their data to the state machine. - # - # An alternative approach would be to disable automatic polling by Home - # Assistant and take control ourselves. This works great for polling as now - # we trigger from 1 time update an update to all entities. However it gets - # tricky from inside async_turn_on and async_turn_off. - # - # If automatic polling is enabled, Home Assistant will call the entity - # update method after it is done calling all the services. This means that - # when we update, we know all commands have been processed. If we trigger - # the update from inside async_turn_on, the update will not capture the - # changes to the second entity until the next polling update because the - # throttle decorator will prevent the call. - - progress = None - light_progress = set() - group_progress = set() - - async def request_update(is_group, object_id): - """Request an update. - - We will only make 1 request to the server for updating at a time. If a - request is in progress, we will join the request that is in progress. - - This approach is possible because should_poll=True. That means that - Home Assistant will ask lights for updates during a polling cycle or - after it has called a service. - - We keep track of the lights that are waiting for the request to finish. - When new data comes in, we'll trigger an update for all non-waiting - lights. This covers the case where a service is called to enable 2 - lights but in the meanwhile some other light has changed too. - """ - nonlocal progress - - progress_set = group_progress if is_group else light_progress - progress_set.add(object_id) - - if progress is not None: - return await progress - - progress = asyncio.ensure_future(update_bridge()) - result = await progress - progress = None - light_progress.clear() - group_progress.clear() - return result - - async def update_bridge(): - """Update the values of the bridge. - - Will update lights and, if enabled, groups from the bridge. - """ - tasks = [] - tasks.append( - async_update_items( - hass, - config_entry, - bridge, - async_add_entities, - request_update, - False, - cur_lights, - light_progress, - ) - ) - - if allow_groups: - tasks.append( - async_update_items( - hass, - config_entry, - bridge, - async_add_entities, - request_update, - True, - cur_groups, - group_progress, - ) - ) - - await asyncio.wait(tasks) - - await update_bridge() - - -async def async_update_items( - hass, - config_entry, - bridge, - async_add_entities, - request_bridge_update, - is_group, - current, - progress_waiting, -): - """Update either groups or lights from the bridge.""" - if not bridge.authorized: + if not allow_groups: return - if is_group: - api_type = "group" - api = bridge.api.groups - else: - api_type = "light" - api = bridge.api.lights + group_coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + "group", + partial(async_safe_fetch, bridge, bridge.api.groups.update), + SCAN_INTERVAL, + Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + ) + update_groups = partial( + async_update_items, + bridge, + bridge.api.groups, + {}, + async_add_entities, + partial(HueLight, group_coordinator, bridge, True), + ) + + group_coordinator.async_add_listener(update_groups) + await group_coordinator.async_refresh() + + bridge.reset_jobs.append( + lambda: group_coordinator.async_remove_listener(update_groups) + ) + + +async def async_safe_fetch(bridge, fetch_method): + """Safely fetch data.""" try: - start = monotonic() with async_timeout.timeout(4): - await bridge.async_request_call(api.update()) + return await bridge.async_request_call(fetch_method()) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() - return - except (asyncio.TimeoutError, aiohue.AiohueException) as err: - _LOGGER.debug("Failed to fetch %s: %s", api_type, err) + raise UpdateFailed + except (asyncio.TimeoutError, aiohue.AiohueException): + raise UpdateFailed - if not bridge.available: - return - - _LOGGER.error("Unable to reach bridge %s (%s)", bridge.host, err) - bridge.available = False - - for item_id, item in current.items(): - if item_id not in progress_waiting: - item.async_schedule_update_ha_state() - - return - - finally: - _LOGGER.debug( - "Finished %s request in %.3f seconds", api_type, monotonic() - start - ) - - if not bridge.available: - _LOGGER.info("Reconnected to bridge %s", bridge.host) - bridge.available = True +@callback +def async_update_items(bridge, api, current, async_add_entities, create_item): + """Update items.""" new_items = [] for item_id in api: - if item_id not in current: - current[item_id] = HueLight( - api[item_id], request_bridge_update, bridge, is_group - ) + if item_id in current: + continue - new_items.append(current[item_id]) - elif item_id not in progress_waiting: - current[item_id].async_schedule_update_ha_state() + current[item_id] = create_item(api[item_id]) + new_items.append(current[item_id]) - await remove_devices(hass, config_entry, api, current) + bridge.hass.async_create_task(remove_devices(bridge, api, current)) if new_items: async_add_entities(new_items) @@ -251,10 +178,10 @@ async def async_update_items( class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, light, request_bridge_update, bridge, is_group=False): + def __init__(self, coordinator, bridge, is_group, light): """Initialize the light.""" self.light = light - self.async_request_bridge_update = request_bridge_update + self.coordinator = coordinator self.bridge = bridge self.is_group = is_group @@ -289,6 +216,11 @@ class HueLight(Light): """Return the unique ID of this Hue light.""" return self.light.uniqueid + @property + def should_poll(self): + """No polling required.""" + return False + @property def device_id(self): """Return the ID of this Hue light.""" @@ -345,14 +277,10 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return ( - self.bridge.available - and self.bridge.authorized - and ( - self.is_group - or self.bridge.allow_unreachable - or self.light.state["reachable"] - ) + return not self.coordinator.failed_last_update and ( + self.is_group + or self.bridge.allow_unreachable + or self.light.state["reachable"] ) @property @@ -379,7 +307,7 @@ class HueLight(Light): return None return { - "identifiers": {(hue.DOMAIN, self.device_id)}, + "identifiers": {(HUE_DOMAIN, self.device_id)}, "name": self.name, "manufacturer": self.light.manufacturername, # productname added in Hue Bridge API 1.24 @@ -387,9 +315,17 @@ class HueLight(Light): "model": self.light.productname or self.light.modelid, # Not yet exposed as properties in aiohue "sw_version": self.light.raw["swversion"], - "via_device": (hue.DOMAIN, self.bridge.api.config.bridgeid), + "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {"on": True} @@ -440,6 +376,8 @@ class HueLight(Light): else: await self.bridge.async_request_call(self.light.set_state(**command)) + await self.coordinator.async_request_refresh() + async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" command = {"on": False} @@ -463,9 +401,14 @@ class HueLight(Light): else: await self.bridge.async_request_call(self.light.set_state(**command)) + await self.coordinator.async_request_refresh() + async def async_update(self): - """Synchronize state with bridge.""" - await self.async_request_bridge_update(self.is_group, self.light.id) + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() @property def device_state_attributes(self): diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index f2e02d49ecf..5fa2ed68389 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,11 +1,6 @@ """Hue sensor entities.""" from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - SensorManager, - async_setup_entry as shared_async_setup_entry, -) from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, @@ -13,27 +8,18 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity +from .const import DOMAIN as HUE_DOMAIN +from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor + LIGHT_LEVEL_NAME_FORMAT = "{} light level" TEMPERATURE_NAME_FORMAT = "{} temperature" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" - SensorManager.sensor_config_map.update( - { - TYPE_ZLL_LIGHTLEVEL: { - "binary": False, - "name_format": LIGHT_LEVEL_NAME_FORMAT, - "class": HueLightLevel, - }, - TYPE_ZLL_TEMPERATURE: { - "binary": False, - "name_format": TEMPERATURE_NAME_FORMAT, - "class": HueTemperature, - }, - } - ) - await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=False) + await hass.data[HUE_DOMAIN][ + config_entry.entry_id + ].sensor_manager.async_register_component(False, async_add_entities) class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity): @@ -91,3 +77,19 @@ class HueTemperature(GenericHueGaugeSensorEntity): return None return self.sensor.temperature / 100 + + +SENSOR_CONFIG_MAP.update( + { + TYPE_ZLL_LIGHTLEVEL: { + "binary": False, + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + TYPE_ZLL_TEMPERATURE: { + "binary": False, + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + } +) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index f7882b102c0..3db07ba2e5b 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -2,22 +2,19 @@ import asyncio from datetime import timedelta import logging -from time import monotonic from aiohue import AiohueException, Unauthorized from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout -from homeassistant.components import hue -from homeassistant.exceptions import NoEntitySpecifiedError -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow +from homeassistant.core import callback +from homeassistant.helpers import debounce, entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY from .helpers import remove_devices -CURRENT_SENSORS_FORMAT = "{}_current_sensors" -SENSOR_MANAGER_FORMAT = "{}_sensor_manager" - +SENSOR_CONFIG_MAP = {} _LOGGER = logging.getLogger(__name__) @@ -29,22 +26,6 @@ def _device_id(aiohue_sensor): return device_id -async def async_setup_entry(hass, config_entry, async_add_entities, binary=False): - """Set up the Hue sensors from a config entry.""" - sensor_key = CURRENT_SENSORS_FORMAT.format(config_entry.data["host"]) - bridge = hass.data[hue.DOMAIN][config_entry.data["host"]] - hass.data[hue.DOMAIN].setdefault(sensor_key, {}) - - sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) - manager = hass.data[hue.DOMAIN].get(sm_key) - if manager is None: - manager = SensorManager(hass, bridge, config_entry) - hass.data[hue.DOMAIN][sm_key] = manager - - manager.register_component(binary, async_add_entities) - await manager.start() - - class SensorManager: """Class that handles registering and updating Hue sensor entities. @@ -52,84 +33,60 @@ class SensorManager: """ SCAN_INTERVAL = timedelta(seconds=5) - sensor_config_map = {} - def __init__(self, hass, bridge, config_entry): + def __init__(self, bridge): """Initialize the sensor manager.""" - self.hass = hass self.bridge = bridge - self.config_entry = config_entry self._component_add_entities = {} - self._started = False + self.current = {} + self.coordinator = DataUpdateCoordinator( + bridge.hass, + _LOGGER, + "sensor", + self.async_update_data, + self.SCAN_INTERVAL, + debounce.Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + ) - def register_component(self, binary, async_add_entities): + async def async_update_data(self): + """Update sensor data.""" + try: + with async_timeout.timeout(4): + return await self.bridge.async_request_call( + self.bridge.api.sensors.update() + ) + except Unauthorized: + await self.bridge.handle_unauthorized_error() + raise UpdateFailed + except (asyncio.TimeoutError, AiohueException): + raise UpdateFailed + + async def async_register_component(self, binary, async_add_entities): """Register async_add_entities methods for components.""" self._component_add_entities[binary] = async_add_entities - async def start(self): - """Start updating sensors from the bridge on a schedule.""" - # but only if it's not already started, and when we've got both - # async_add_entities methods - if self._started or len(self._component_add_entities) < 2: + if len(self._component_add_entities) < 2: return - self._started = True - _LOGGER.info( - "Starting sensor polling loop with %s second interval", - self.SCAN_INTERVAL.total_seconds(), + # We have all components available, start the updating. + self.coordinator.async_add_listener(self.async_update_items) + self.bridge.reset_jobs.append( + lambda: self.coordinator.async_remove_listener(self.async_update_items) ) + await self.coordinator.async_refresh() - async def async_update_bridge(now): - """Will update sensors from the bridge.""" - - # don't update when we are not authorized - if not self.bridge.authorized: - return - - await self.async_update_items() - - async_track_point_in_utc_time( - self.hass, async_update_bridge, utcnow() + self.SCAN_INTERVAL - ) - - await async_update_bridge(None) - - async def async_update_items(self): + @callback + def async_update_items(self): """Update sensors from the bridge.""" api = self.bridge.api.sensors - try: - start = monotonic() - with async_timeout.timeout(4): - await self.bridge.async_request_call(api.update()) - except Unauthorized: - await self.bridge.handle_unauthorized_error() + if len(self._component_add_entities) < 2: return - except (asyncio.TimeoutError, AiohueException) as err: - _LOGGER.debug("Failed to fetch sensor: %s", err) - - if not self.bridge.available: - return - - _LOGGER.error("Unable to reach bridge %s (%s)", self.bridge.host, err) - self.bridge.available = False - - return - - finally: - _LOGGER.debug( - "Finished sensor request in %.3f seconds", monotonic() - start - ) - - if not self.bridge.available: - _LOGGER.info("Reconnected to bridge %s", self.bridge.host) - self.bridge.available = True new_sensors = [] new_binary_sensors = [] primary_sensor_devices = {} - sensor_key = CURRENT_SENSORS_FORMAT.format(self.config_entry.data["host"]) - current = self.hass.data[hue.DOMAIN][sensor_key] + current = self.current # Physical Hue motion sensors present as three sensors in the API: a # presence sensor, a temperature sensor, and a light level sensor. Of @@ -155,11 +112,10 @@ class SensorManager: for item_id in api: existing = current.get(api[item_id].uniqueid) if existing is not None: - self.hass.async_create_task(existing.async_maybe_update_ha_state()) continue primary_sensor = None - sensor_config = self.sensor_config_map.get(api[item_id].type) + sensor_config = SENSOR_CONFIG_MAP.get(api[item_id].type) if sensor_config is None: continue @@ -177,22 +133,19 @@ class SensorManager: else: new_sensors.append(current[api[item_id].uniqueid]) - await remove_devices( - self.hass, - self.config_entry, - [value.uniqueid for value in api.values()], - current, + self.bridge.hass.async_create_task( + remove_devices( + self.bridge, [value.uniqueid for value in api.values()], current, + ) ) - async_add_sensor_entities = self._component_add_entities.get(False) - async_add_binary_entities = self._component_add_entities.get(True) - if new_sensors and async_add_sensor_entities: - async_add_sensor_entities(new_sensors) - if new_binary_sensors and async_add_binary_entities: - async_add_binary_entities(new_binary_sensors) + if new_sensors: + self._component_add_entities[False](new_sensors) + if new_binary_sensors: + self._component_add_entities[True](new_binary_sensors) -class GenericHueSensor: +class GenericHueSensor(entity.Entity): """Representation of a Hue sensor.""" should_poll = False @@ -230,10 +183,8 @@ class GenericHueSensor: @property def available(self): """Return if sensor is available.""" - return ( - self.bridge.available - and self.bridge.authorized - and (self.bridge.allow_unreachable or self.sensor.config["reachable"]) + return not self.bridge.sensor_manager.coordinator.failed_last_update and ( + self.bridge.allow_unreachable or self.sensor.config["reachable"] ) @property @@ -241,15 +192,24 @@ class GenericHueSensor: """Return detail of available software updates for this device.""" return self.primary_sensor.raw.get("swupdate", {}).get("state") - async def async_maybe_update_ha_state(self): - """Try to update Home Assistant with current state of entity. + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.bridge.sensor_manager.coordinator.async_add_listener( + self.async_write_ha_state + ) - But if it's not been added to hass yet, then don't throw an error. + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + self.bridge.sensor_manager.coordinator.async_remove_listener( + self.async_write_ha_state + ) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. """ - try: - await self._async_update_ha_state() - except (RuntimeError, NoEntitySpecifiedError): - _LOGGER.debug("Hue sensor update requested before it has been added.") + await self.bridge.sensor_manager.coordinator.coordinator.async_request_refresh() @property def device_info(self): @@ -258,12 +218,12 @@ class GenericHueSensor: Links individual entities together in the hass device registry. """ return { - "identifiers": {(hue.DOMAIN, self.device_id)}, + "identifiers": {(HUE_DOMAIN, self.device_id)}, "name": self.primary_sensor.name, "manufacturer": self.primary_sensor.manufacturername, "model": (self.primary_sensor.productname or self.primary_sensor.modelid), "sw_version": self.primary_sensor.swversion, - "via_device": (hue.DOMAIN, self.bridge.api.config.bridgeid), + "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py new file mode 100644 index 00000000000..5bacbdb7d11 --- /dev/null +++ b/homeassistant/helpers/debounce.py @@ -0,0 +1,77 @@ +"""Debounce helper.""" +import asyncio +from logging import Logger +from typing import Any, Awaitable, Callable, Optional + +from homeassistant.core import HomeAssistant, callback + + +class Debouncer: + """Class to rate limit calls to a specific command.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + cooldown: float, + immediate: bool, + function: Optional[Callable[..., Awaitable[Any]]] = None, + ): + """Initialize debounce. + + immediate: indicate if the function needs to be called right away and + wait 0.3s until executing next invocation. + function: optional and can be instantiated later. + """ + self.hass = hass + self.logger = logger + self.function = function + self.cooldown = cooldown + self.immediate = immediate + self._timer_task: Optional[asyncio.TimerHandle] = None + self._execute_at_end_of_timer: bool = False + + async def async_call(self) -> None: + """Call the function.""" + assert self.function is not None + + if self._timer_task: + if not self._execute_at_end_of_timer: + self._execute_at_end_of_timer = True + + return + + if self.immediate: + await self.hass.async_add_job(self.function) # type: ignore + else: + self._execute_at_end_of_timer = True + + self._timer_task = self.hass.loop.call_later( + self.cooldown, + lambda: self.hass.async_create_task(self._handle_timer_finish()), + ) + + async def _handle_timer_finish(self) -> None: + """Handle a finished timer.""" + assert self.function is not None + + self._timer_task = None + + if not self._execute_at_end_of_timer: + return + + self._execute_at_end_of_timer = False + + try: + await self.hass.async_add_job(self.function) # type: ignore + except Exception: # pylint: disable=broad-except + self.logger.exception("Unexpected exception from %s", self.function) + + @callback + def async_cancel(self) -> None: + """Cancel any scheduled call.""" + if self._timer_task: + self._timer_task.cancel() + self._timer_task = None + + self._execute_at_end_of_timer = False diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b3c8af6f50c..74faca6a1d2 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -225,7 +225,7 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @callback @bind_hass def async_track_point_in_utc_time( - hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime + hass: HomeAssistant, action: Callable[..., Any], point_in_time: datetime ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" # Ensure point_in_time is UTC diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py new file mode 100644 index 00000000000..dc990637e31 --- /dev/null +++ b/homeassistant/helpers/update_coordinator.py @@ -0,0 +1,135 @@ +"""Helpers to help coordinate updates.""" +import asyncio +from datetime import datetime, timedelta +import logging +from time import monotonic +from typing import Any, Awaitable, Callable, List, Optional + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +from .debounce import Debouncer + + +class UpdateFailed(Exception): + """Raised when an update has failed.""" + + +class DataUpdateCoordinator: + """Class to manage fetching data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + name: str, + update_method: Callable[[], Awaitable], + update_interval: timedelta, + request_refresh_debouncer: Debouncer, + ): + """Initialize global data updater.""" + self.hass = hass + self.logger = logger + self.name = name + self.update_method = update_method + self.update_interval = update_interval + + self.data: Optional[Any] = None + + self._listeners: List[CALLBACK_TYPE] = [] + self._unsub_refresh: Optional[CALLBACK_TYPE] = None + self._request_refresh_task: Optional[asyncio.TimerHandle] = None + self.failed_last_update = False + self._debounced_refresh = request_refresh_debouncer + request_refresh_debouncer.function = self._async_do_refresh + + @callback + def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Listen for data updates.""" + schedule_refresh = not self._listeners + + self._listeners.append(update_callback) + + # This is the first listener, set up interval. + if schedule_refresh: + self._schedule_refresh() + + @callback + def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Remove data update.""" + self._listeners.remove(update_callback) + + if not self._listeners and self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + async def async_refresh(self) -> None: + """Refresh the data.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + await self._async_do_refresh() + + @callback + def _schedule_refresh(self) -> None: + """Schedule a refresh.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, self._handle_refresh_interval, utcnow() + self.update_interval + ) + + async def _handle_refresh_interval(self, _now: datetime) -> None: + """Handle a refresh interval occurrence.""" + self._unsub_refresh = None + await self._async_do_refresh() + + async def async_request_refresh(self) -> None: + """Request a refresh. + + Refresh will wait a bit to see if it can batch them. + """ + await self._debounced_refresh.async_call() + + async def _async_do_refresh(self) -> None: + """Time to update.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + self._debounced_refresh.async_cancel() + + try: + start = monotonic() + self.data = await self.update_method() + + except UpdateFailed as err: + if not self.failed_last_update: + self.logger.error("Error fetching %s data: %s", self.name, err) + self.failed_last_update = True + + except Exception as err: # pylint: disable=broad-except + self.failed_last_update = True + self.logger.exception( + "Unexpected error fetching %s data: %s", self.name, err + ) + + else: + if self.failed_last_update: + self.failed_last_update = False + self.logger.info("Fetching %s data recovered") + + finally: + self.logger.debug( + "Finished fetching %s data in %.3f seconds", + self.name, + monotonic() - start, + ) + self._schedule_refresh() + + for update_callback in self._listeners: + update_callback() diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py new file mode 100644 index 00000000000..49cd953a697 --- /dev/null +++ b/tests/components/hue/conftest.py @@ -0,0 +1,11 @@ +"""Test helpers for Hue.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def no_request_delay(): + """Make the request refresh delay 0 for instant tests.""" + with patch("homeassistant.components.hue.light.REQUEST_REFRESH_DELAY", 0): + yield diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 0f3e197b979..df3fe5f8998 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -179,11 +179,13 @@ LIGHT_GAMUT_TYPE = "A" def mock_bridge(hass): """Mock a Hue bridge.""" bridge = Mock( + hass=hass, available=True, authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), + reset_jobs=[], spec=hue.HueBridge, ) bridge.mock_requests = [] @@ -218,7 +220,6 @@ def mock_bridge(hass): async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) - hass.data[hue.DOMAIN] = {"mock-host": mock_bridge} config_entry = config_entries.ConfigEntry( 1, hue.DOMAIN, @@ -228,6 +229,8 @@ async def setup_bridge(hass, mock_bridge): config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, ) + mock_bridge.config_entry = config_entry + hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} await hass.config_entries.async_forward_entry_setup(config_entry, "light") # To flush out the service call to update the group await hass.async_block_till_done() @@ -363,8 +366,8 @@ async def test_new_group_discovered(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) - # 2x group update, 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 5 + # 2x group update, 1x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 3 new_group = hass.states.get("light.group_3") @@ -443,8 +446,8 @@ async def test_group_removed(hass, mock_bridge): "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) - # 2x group update, 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 5 + # 2x group update, 1x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 1 group = hass.states.get("light.group_1") @@ -524,8 +527,8 @@ async def test_other_group_update(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) - # 2x group update, 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 5 + # 2x group update, 1x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 2 group_2 = hass.states.get("light.group_2") @@ -599,7 +602,6 @@ async def test_update_timeout(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False async def test_update_unauthorized(hass, mock_bridge): @@ -701,7 +703,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(allow_unreachable=False), is_group=False, ) @@ -715,7 +717,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(allow_unreachable=True), is_group=False, ) @@ -729,7 +731,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(allow_unreachable=False), is_group=True, ) @@ -746,7 +748,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(), is_group=False, ) @@ -760,7 +762,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(), is_group=False, ) @@ -774,7 +776,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(), is_group=False, ) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ad927767c30..78255116831 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,7 +1,6 @@ """Philips Hue sensors platform tests.""" import asyncio from collections import deque -import datetime import logging from unittest.mock import Mock @@ -252,16 +251,19 @@ SENSOR_RESPONSE = { } -def create_mock_bridge(): +def create_mock_bridge(hass): """Create a mock Hue bridge.""" bridge = Mock( + hass=hass, available=True, authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), + reset_jobs=[], spec=hue.HueBridge, ) + bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) bridge.mock_requests = [] # We're using a deque so we can schedule multiple responses # and also means that `popleft()` will blow up if we get more updates @@ -289,13 +291,7 @@ def create_mock_bridge(): @pytest.fixture def mock_bridge(hass): """Mock a Hue bridge.""" - return create_mock_bridge() - - -@pytest.fixture -def increase_scan_interval(hass): - """Increase the SCAN_INTERVAL to prevent unexpected scans during tests.""" - hue_sensor_base.SensorManager.SCAN_INTERVAL = datetime.timedelta(days=365) + return create_mock_bridge(hass) async def setup_bridge(hass, mock_bridge, hostname=None): @@ -303,7 +299,6 @@ async def setup_bridge(hass, mock_bridge, hostname=None): if hostname is None: hostname = "mock-host" hass.config.components.add(hue.DOMAIN) - hass.data[hue.DOMAIN] = {hostname: mock_bridge} config_entry = config_entries.ConfigEntry( 1, hue.DOMAIN, @@ -313,6 +308,8 @@ async def setup_bridge(hass, mock_bridge, hostname=None): config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, ) + mock_bridge.config_entry = config_entry + hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") # and make sure it completes before going further @@ -330,7 +327,7 @@ async def test_no_sensors(hass, mock_bridge): async def test_sensors_with_multiple_bridges(hass, mock_bridge): """Test the update_items function with some sensors.""" - mock_bridge_2 = create_mock_bridge() + mock_bridge_2 = create_mock_bridge(hass) mock_bridge_2.mock_sensor_responses.append( { "1": PRESENCE_SENSOR_3_PRESENT, @@ -412,11 +409,7 @@ async def test_new_sensor_discovered(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host") - sm = hass.data[hue.DOMAIN][sm_key] - await sm.async_update_items() - - # To flush out the service call to update the group + await mock_bridge.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 2 @@ -443,9 +436,7 @@ async def test_sensor_removed(hass, mock_bridge): mock_bridge.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys}) # Force updates to run again - sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host") - sm = hass.data[hue.DOMAIN][sm_key] - await sm.async_update_items() + await mock_bridge.sensor_manager.coordinator.async_refresh() # To flush out the service call to update the group await hass.async_block_till_done() @@ -466,7 +457,6 @@ async def test_update_timeout(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False async def test_update_unauthorized(hass, mock_bridge): diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py new file mode 100644 index 00000000000..d7629a393a9 --- /dev/null +++ b/tests/helpers/test_debounce.py @@ -0,0 +1,62 @@ +"""Tests for debounce.""" +from asynctest import CoroutineMock + +from homeassistant.helpers import debounce + + +async def test_immediate_works(hass): + """Test immediate works.""" + calls = [] + debouncer = debounce.Debouncer( + hass, None, 0.01, True, CoroutineMock(side_effect=lambda: calls.append(None)) + ) + + await debouncer.async_call() + assert len(calls) == 1 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is False + + await debouncer.async_call() + assert len(calls) == 1 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is True + + debouncer.async_cancel() + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False + + await debouncer.async_call() + assert len(calls) == 2 + await debouncer._handle_timer_finish() + assert len(calls) == 2 + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False + + +async def test_not_immediate_works(hass): + """Test immediate works.""" + calls = [] + debouncer = debounce.Debouncer( + hass, None, 0.01, False, CoroutineMock(side_effect=lambda: calls.append(None)) + ) + + await debouncer.async_call() + assert len(calls) == 0 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is True + + await debouncer.async_call() + assert len(calls) == 0 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is True + + debouncer.async_cancel() + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False + + await debouncer.async_call() + assert len(calls) == 0 + await debouncer._handle_timer_finish() + assert len(calls) == 1 + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False From 0c1acc51a45f512a38374654395baae5f19a762c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 1 Feb 2020 00:31:40 +0000 Subject: [PATCH 039/378] [ci skip] Translation update --- .../components/elgato/.translations/ru.json | 4 +- .../garmin_connect/.translations/es.json | 24 ++++++++++++ .../components/linky/.translations/es.json | 1 + .../components/mikrotik/.translations/es.json | 37 +++++++++++++++++++ .../components/mikrotik/.translations/no.json | 37 +++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/garmin_connect/.translations/es.json create mode 100644 homeassistant/components/mikrotik/.translations/es.json create mode 100644 homeassistant/components/mikrotik/.translations/no.json diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json index 454e6e78d84..1663ea4d23a 100644 --- a/homeassistant/components/elgato/.translations/ru.json +++ b/homeassistant/components/elgato/.translations/ru.json @@ -19,9 +19,9 @@ }, "zeroconf_confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Key Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgado Key Light" + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgato Key Light" } }, - "title": "Elgado Key Light" + "title": "Elgato Key Light" } } \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/es.json b/homeassistant/components/garmin_connect/.translations/es.json new file mode 100644 index 00000000000..989d86dbc35 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cuenta ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntelo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Introduzca sus credenciales.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json index 511f3c9d8e5..334c5eaa0f0 100644 --- a/homeassistant/components/linky/.translations/es.json +++ b/homeassistant/components/linky/.translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "username_exists": "Cuenta ya configurada" }, "error": { diff --git a/homeassistant/components/mikrotik/.translations/es.json b/homeassistant/components/mikrotik/.translations/es.json new file mode 100644 index 00000000000..61bce851f42 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Conexi\u00f3n fallida", + "name_exists": "El nombre ya existe", + "wrong_credentials": "Credenciales incorrectas" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario", + "verify_ssl": "Usar ssl" + }, + "title": "Configurar el router Mikrotik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Habilitar ping ARP", + "detection_time": "Considere el intervalo de inicio", + "force_dhcp": "Forzar el escaneo usando DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/no.json b/homeassistant/components/mikrotik/.translations/no.json new file mode 100644 index 00000000000..f842dd148ec --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislykket", + "name_exists": "Navnet eksisterer", + "wrong_credentials": "Feil legitimasjon" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn", + "verify_ssl": "Bruk ssl" + }, + "title": "Konfigurere Mikrotik-ruter" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Aktiver ARP-ping", + "detection_time": "Vurder hjemmeintervall", + "force_dhcp": "Tving skanning ved hjelp av DHCP" + } + } + } + } +} \ No newline at end of file From 0a90b01e77cd833b1930f9bd59fa8433bfab28bb Mon Sep 17 00:00:00 2001 From: Dan Lehman <53992354+DanTLehman@users.noreply.github.com> Date: Sat, 1 Feb 2020 16:42:37 +1100 Subject: [PATCH 040/378] Updated wemo lights fix for #31360 (#31369) --- homeassistant/components/wemo/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 7d2cf9afc43..5988019e66f 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -114,7 +114,7 @@ class WemoLight(Light): return { "name": self.wemo.name, "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, - "model": self.wemo.device_type, + "model": type(self.wemo).__name__, "manufacturer": "Belkin", } From 26415f6abd5001c76b21aefdfcc2a2acdbfc1af6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 1 Feb 2020 10:01:42 +0100 Subject: [PATCH 041/378] Fix auto_bypass in alarmdecoder (#30961) * Fix auto_bypass in alarmdecoder * Address review comments: used dict[key] and renamed variable * Use dict[key] for required or optional config keys with default values --- homeassistant/components/alarmdecoder/__init__.py | 11 +++++++---- .../components/alarmdecoder/alarm_control_panel.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 833156e98b2..a990de9bf98 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -118,11 +118,12 @@ def setup(hass, config): conf = config.get(DOMAIN) restart = False - device = conf.get(CONF_DEVICE) - display = conf.get(CONF_PANEL_DISPLAY) + device = conf[CONF_DEVICE] + display = conf[CONF_PANEL_DISPLAY] + auto_bypass = conf[CONF_AUTO_BYPASS] zones = conf.get(CONF_ZONES) - device_type = device.get(CONF_DEVICE_TYPE) + device_type = device[CONF_DEVICE_TYPE] host = DEFAULT_DEVICE_HOST port = DEFAULT_DEVICE_PORT path = DEFAULT_DEVICE_PATH @@ -204,7 +205,9 @@ def setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) - load_platform(hass, "alarm_control_panel", DOMAIN, conf, config) + load_platform( + hass, "alarm_control_panel", DOMAIN, {CONF_AUTO_BYPASS: auto_bypass}, config + ) if zones: load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 70f3e67e15b..e217bcb6cf9 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE +from . import CONF_AUTO_BYPASS, DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) @@ -35,13 +35,17 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up for AlarmDecoder alarm panels.""" - device = AlarmDecoderAlarmPanel(discovery_info["autobypass"]) - add_entities([device]) + if discovery_info is None: + return + + auto_bypass = discovery_info[CONF_AUTO_BYPASS] + entity = AlarmDecoderAlarmPanel(auto_bypass) + add_entities([entity]) def alarm_toggle_chime_handler(service): """Register toggle chime handler.""" code = service.data.get(ATTR_CODE) - device.alarm_toggle_chime(code) + entity.alarm_toggle_chime(code) hass.services.register( DOMAIN, @@ -53,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def alarm_keypress_handler(service): """Register keypress handler.""" keypress = service.data[ATTR_KEYPRESS] - device.alarm_keypress(keypress) + entity.alarm_keypress(keypress) hass.services.register( DOMAIN, From f584df46b7ccf691f30cddc9f3816d043e7cfbae Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 1 Feb 2020 01:09:52 -0800 Subject: [PATCH 042/378] Add totalconnect zones as binary sensors (#28712) * Bump skybellpy to 0.4.0 * Bump skybellpy to 0.4.0 in requirements_all.txt * Added extra states for STATE_ALARM_TRIGGERED to allow users to know if it is a burglar or fire or carbon monoxide so automations can take appropriate actions. Updated TotalConnect component to handle these new states. * Fix const import * Fix const import * Fix const imports * Bump total-connect-client to 0.26. * Catch details of alarm trigger in state attributes. Also bumps total_connect_client to 0.27. * Change state_attributes() to device_state_attributes() * Move totalconnect component toward being a multi-platform integration. Bump total_connect_client to 0.28. * add missing total-connect alarm state mappings * Made recommended changes of MartinHjelmare at https://github.com/home-assistant/home-assistant/pull/24427 * Update __init__.py * Updates per MartinHjelmare comments * flake8/pydocstyle fixes * removed . at end of log message * added blank line between logging and voluptuous * more fixes * Adding totalconnect zones as HA binary_sensors * fix manifest.json * flake8/pydocstyle fixes. Added codeowner. * Update formatting per @springstan guidance. * Fixed pylint * Add zone ID to log message for easier troubleshooting * Account for bypassed zones in update() * More status handling fixes. * Fixed flake8 error * Another attempt at black/isort fixes. * Bump total-connect-client to 0.50. Simplify code using new functions in total-connect-client package instead of importing constants. Run black and isort. * Fix manifest file * Another manifest fix * one more manifest fix * more manifest changes. * sync up * fix indent * one more pylint fix * Hopefully the last pylint fix * make variable names understandable * create and fill dict in one step * Fix name and attributes * rename to logical variable in alarm_control_panel * Remove location_name from alarm_control_panel attributes since it is already the name of the alarm. * Multiple fixes to improve code per @springstan suggestions * Update homeassistant/components/totalconnect/binary_sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Multiple changes per @MartinHjelmare review * simplify alarm adding * Fix binary_sensor.py is_on Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/totalconnect/__init__.py | 2 +- .../totalconnect/alarm_control_panel.py | 51 ++++++----- .../components/totalconnect/binary_sensor.py | 90 +++++++++++++++++++ .../components/totalconnect/manifest.json | 4 +- requirements_all.txt | 2 +- 6 files changed, 120 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/totalconnect/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 6983d13fc8b..60501876cd9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -354,6 +354,7 @@ homeassistant/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck +homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 020f2d9c07f..e6cfbbc629a 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -24,7 +24,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -TOTALCONNECT_PLATFORMS = ["alarm_control_panel"] +TOTALCONNECT_PLATFORMS = ["alarm_control_panel", "binary_sensor"] def setup(hass, config): diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index ed77fc4eea0..b255132a365 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -32,10 +32,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): client = hass.data[TOTALCONNECT_DOMAIN].client - for location in client.locations: - location_id = location.get("LocationID") - name = location.get("LocationName") - alarms.append(TotalConnectAlarm(name, location_id, client)) + for location_id, location in client.locations.items(): + location_name = location.location_name + alarms.append(TotalConnectAlarm(location_name, location_id, client)) add_entities(alarms) @@ -72,35 +71,35 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): def update(self): """Return the state of the device.""" - status = self._client.get_armed_status(self._name) + status = self._client.get_armed_status(self._location_id) attr = { "location_name": self._name, "location_id": self._location_id, - "ac_loss": self._client.ac_loss, - "low_battery": self._client.low_battery, + "ac_loss": self._client.locations[self._location_id].ac_loss, + "low_battery": self._client.locations[self._location_id].low_battery, + "cover_tampered": self._client.locations[ + self._location_id + ].is_cover_tampered, "triggered_source": None, "triggered_zone": None, } - if status == self._client.DISARMED: + if status in (self._client.DISARMED, self._client.DISARMED_BYPASS): state = STATE_ALARM_DISARMED - elif status == self._client.DISARMED_BYPASS: - state = STATE_ALARM_DISARMED - elif status == self._client.ARMED_STAY: - state = STATE_ALARM_ARMED_HOME - elif status == self._client.ARMED_STAY_INSTANT: - state = STATE_ALARM_ARMED_HOME - elif status == self._client.ARMED_STAY_INSTANT_BYPASS: + elif status in ( + self._client.ARMED_STAY, + self._client.ARMED_STAY_INSTANT, + self._client.ARMED_STAY_INSTANT_BYPASS, + ): state = STATE_ALARM_ARMED_HOME elif status == self._client.ARMED_STAY_NIGHT: state = STATE_ALARM_ARMED_NIGHT - elif status == self._client.ARMED_AWAY: - state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_AWAY_BYPASS: - state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_AWAY_INSTANT: - state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_AWAY_INSTANT_BYPASS: + elif status in ( + self._client.ARMED_AWAY, + self._client.ARMED_AWAY_BYPASS, + self._client.ARMED_AWAY_INSTANT, + self._client.ARMED_AWAY_INSTANT_BYPASS, + ): state = STATE_ALARM_ARMED_AWAY elif status == self._client.ARMED_CUSTOM_BYPASS: state = STATE_ALARM_ARMED_CUSTOM_BYPASS @@ -128,16 +127,16 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """Send disarm command.""" - self._client.disarm(self._name) + self._client.disarm(self._location_id) def alarm_arm_home(self, code=None): """Send arm home command.""" - self._client.arm_stay(self._name) + self._client.arm_stay(self._location_id) def alarm_arm_away(self, code=None): """Send arm away command.""" - self._client.arm_away(self._name) + self._client.arm_away(self._location_id) def alarm_arm_night(self, code=None): """Send arm night command.""" - self._client.arm_stay_night(self._name) + self._client.arm_stay_night(self._location_id) diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py new file mode 100644 index 00000000000..28bd58cfff8 --- /dev/null +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -0,0 +1,90 @@ +"""Interfaces with TotalConnect sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) + +from . import DOMAIN as TOTALCONNECT_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for a TotalConnect device.""" + if discovery_info is None: + return + + sensors = [] + + client_locations = hass.data[TOTALCONNECT_DOMAIN].client.locations + + for location_id, location in client_locations.items(): + for zone_id, zone in location.zones.items(): + sensors.append(TotalConnectBinarySensor(zone_id, location_id, zone)) + add_entities(sensors, True) + + +class TotalConnectBinarySensor(BinarySensorDevice): + """Represent an TotalConnect zone.""" + + def __init__(self, zone_id, location_id, zone): + """Initialize the TotalConnect status.""" + self._zone_id = zone_id + self._location_id = location_id + self._zone = zone + self._name = self._zone.description + self._unique_id = f"{location_id} {zone_id}" + self._is_on = None + self._is_tampered = None + self._is_low_battery = None + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the device.""" + return self._name + + def update(self): + """Return the state of the device.""" + self._is_tampered = self._zone.is_tampered() + self._is_low_battery = self._zone.is_low_battery() + + if self._zone.is_faulted() or self._zone.is_triggered(): + self._is_on = True + else: + self._is_on = False + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._is_on + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + if self._zone.is_type_security(): + return DEVICE_CLASS_DOOR + if self._zone.is_type_fire(): + return DEVICE_CLASS_SMOKE + if self._zone.is_type_carbon_monoxide(): + return DEVICE_CLASS_GAS + return None + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + "zone_id": self._zone_id, + "location_id": self._location_id, + "low_battery": self._is_low_battery, + "tampered": self._is_tampered, + } + return attributes diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 6b2119f1cf5..967115e721a 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Honeywell Total Connect Alarm", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==0.28"], + "requirements": ["total_connect_client==0.50"], "dependencies": [], - "codeowners": [] + "codeowners": ["@austinmroczek"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a43c7c8277..87a2b0d8f1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ todoist-python==8.0.0 toonapilib==3.2.4 # homeassistant.components.totalconnect -total_connect_client==0.28 +total_connect_client==0.50 # homeassistant.components.tplink_lte tp-connected==0.0.4 From 79495d9f3a459e691ef20b7a3affd9fe8f413e9b Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sat, 1 Feb 2020 11:40:48 +0100 Subject: [PATCH 043/378] Add Tahoma lock support (#31311) * Added lock support for tahoma * Fixed logging to conform with pylint W1201 and E1205. * Implemented @springstan suggestions. * Fixed a typo... * Implemented @springstan suggestions (v2). * Implemented @springstan suggestions (v3). --- homeassistant/components/tahoma/__init__.py | 3 +- homeassistant/components/tahoma/lock.py | 90 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tahoma/lock.py diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 0d74d6018a5..f14e3019ac0 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -TAHOMA_COMPONENTS = ["scene", "sensor", "cover", "switch", "binary_sensor"] +TAHOMA_COMPONENTS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"] TAHOMA_TYPES = { "io:AwningValanceIOComponent": "cover", @@ -52,6 +52,7 @@ TAHOMA_TYPES = { "io:VerticalExteriorAwningIOComponent": "cover", "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", + "opendoors:OpenDoorsSmartLockComponent": "lock", "rtds:RTDSContactSensor": "sensor", "rtds:RTDSMotionSensor": "sensor", "rtds:RTDSSmokeSensor": "smoke", diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py new file mode 100644 index 00000000000..e320fd9e13d --- /dev/null +++ b/homeassistant/components/tahoma/lock.py @@ -0,0 +1,90 @@ +"""Support for Tahoma lock.""" +from datetime import timedelta +import logging + +from homeassistant.components.lock import LockDevice +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED + +from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=120) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Tahoma lock.""" + controller = hass.data[TAHOMA_DOMAIN]["controller"] + devices = [] + for device in hass.data[TAHOMA_DOMAIN]["devices"]["lock"]: + devices.append(TahomaLock(device, controller)) + add_entities(devices, True) + + +class TahomaLock(TahomaDevice, LockDevice): + """Representation a Tahoma lock.""" + + def __init__(self, tahoma_device, controller): + """Initialize the device.""" + super().__init__(tahoma_device, controller) + self._lock_status = None + self._available = False + self._battery_level = None + self._name = None + + def update(self): + """Update method.""" + self.controller.get_states([self.tahoma_device]) + self._battery_level = self.tahoma_device.active_states["core:BatteryState"] + self._name = self.tahoma_device.active_states["core:NameState"] + if self._battery_level == "low": + _LOGGER.warning("Lock %s has low battery", self._name) + if self._battery_level == "verylow": + _LOGGER.error("Lock %s has very low battery", self._name) + if ( + self.tahoma_device.active_states.get("core:LockedUnlockedState") + == STATE_LOCKED + ): + self._lock_status = STATE_LOCKED + else: + self._lock_status = STATE_UNLOCKED + self._available = ( + self.tahoma_device.active_states.get("core:AvailabilityState") + == "available" + ) + + def unlock(self, **kwargs): + """Unlock method.""" + _LOGGER.debug("Unlocking %s", self._name) + self.apply_action("unlock") + + def lock(self, **kwargs): + """Lock method.""" + _LOGGER.debug("Locking %s", self._name) + self.apply_action("lock") + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def available(self): + """Return True if the lock is available.""" + return self._available + + @property + def is_locked(self): + """Return True if the lock is locked.""" + return self._lock_status == STATE_LOCKED + + @property + def device_state_attributes(self): + """Return the lock state attributes.""" + attr = { + ATTR_BATTERY_LEVEL: self._battery_level, + } + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + return attr From dc5ca461a99ec078ccb44eace22205a6eb2ec80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 17:12:46 +0200 Subject: [PATCH 044/378] Run mypy through a pyenv/virtualenv enabler wrapper script (#30922) --- .pre-commit-config-all.yaml | 59 -------------------------------- .pre-commit-config.yaml | 20 +++++++---- azure-pipelines-ci.yml | 8 ++--- requirements_test_pre_commit.txt | 2 +- script/gen_requirements_all.py | 2 +- script/run-in-env.sh | 18 ++++++++++ tox.ini | 2 +- 7 files changed, 38 insertions(+), 73 deletions(-) delete mode 100644 .pre-commit-config-all.yaml create mode 100755 script/run-in-env.sh diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml deleted file mode 100644 index a6b882e617b..00000000000 --- a/.pre-commit-config-all.yaml +++ /dev/null @@ -1,59 +0,0 @@ -# This configuration includes the full set of hooks we use. In -# addition to the defaults (see .pre-commit-config.yaml), this -# includes hooks that require our development and test dependencies -# installed and the virtualenv containing them active by the time -# pre-commit runs to produce correct results. -# -# If this is not a problem for your workflow, using this config is -# recommended, install it with -# pre-commit install --config .pre-commit-config-all.yaml -# Otherwise, see the default .pre-commit-config.yaml for a lighter one. - -repos: -- repo: https://github.com/psf/black - rev: 19.10b0 - hooks: - - id: black - args: - - --safe - - --quiet - files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ -- repo: https://github.com/PyCQA/flake8 - rev: 3.7.9 - hooks: - - id: flake8 - additional_dependencies: - - flake8-docstrings==1.5.0 - - pydocstyle==5.0.2 - files: ^(homeassistant|script|tests)/.+\.py$ -- repo: https://github.com/PyCQA/bandit - rev: 1.6.2 - hooks: - - id: bandit - args: - - --quiet - - --format=custom - - --configfile=tests/bandit.yaml - files: ^(homeassistant|script|tests)/.+\.py$ -- repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 - hooks: - - id: isort -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 - hooks: - - id: check-json -# Using a local "system" mypy instead of the mypy hook, because its -# results depend on what is installed. And the mypy hook runs in a -# virtualenv of its own, meaning we'd need to install and maintain -# another set of our dependencies there... no. Use the "system" one -# and reuse the environment that is set up anyway already instead. -- repo: local - hooks: - - id: mypy - name: mypy - entry: mypy - language: system - types: [python] - require_serial: true - files: ^homeassistant/.+\.py$ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05601865691..a340aa7ae67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,3 @@ -# This configuration includes the default, minimal set of hooks to be -# run on all commits. It requires no specific setup and one can just -# start using pre-commit with it. -# -# See .pre-commit-config-all.yaml for a more complete one that comes -# with a better coverage at the cost of some specific setup needed. - repos: - repo: https://github.com/psf/black rev: 19.10b0 @@ -48,3 +41,16 @@ repos: rev: v2.4.0 hooks: - id: check-json +- repo: local + hooks: + # Run mypy through our wrapper script in order to get the possible + # pyenv and/or virtualenv activated; it may not have been e.g. if + # committing from a GUI tool that was not launched from an activated + # shell. + - id: mypy + name: mypy + entry: script/run-in-env.sh mypy + language: script + types: [python] + require_serial: true + files: ^homeassistant/.+\.py$ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 0474bf77489..4c6a353d775 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -43,7 +43,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks --config .pre-commit-config-all.yaml + pre-commit install-hooks - script: | . venv/bin/activate pre-commit run codespell --all-files @@ -98,7 +98,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks --config .pre-commit-config-all.yaml + pre-commit install-hooks - script: | . venv/bin/activate pre-commit run black --all-files --show-diff-on-failure @@ -194,8 +194,8 @@ stages: . venv/bin/activate pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks --config .pre-commit-config-all.yaml + pre-commit install-hooks - script: | . venv/bin/activate - pre-commit run --config .pre-commit-config-all.yaml mypy --all-files + pre-commit run mypy --all-files displayName: 'Run mypy' diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 8af2cbb6123..ef10641608d 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,4 +1,4 @@ -# Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit +# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.6.2 black==19.10b0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3b30bf04363..1bf9031a536 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -253,7 +253,7 @@ def requirements_test_output(reqs): def requirements_pre_commit_output(): """Generate output for pre-commit dependencies.""" - source = ".pre-commit-config-all.yaml" + source = ".pre-commit-config.yaml" pre_commit_conf = load_yaml(source) reqs = [] for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")): diff --git a/script/run-in-env.sh b/script/run-in-env.sh new file mode 100755 index 00000000000..586f59d717a --- /dev/null +++ b/script/run-in-env.sh @@ -0,0 +1,18 @@ +#!/bin/sh -eu + +# Activate pyenv and virtualenv if present, then run the specified command + +# pyenv, pyenv-virtualenv +if [ -s .python-version ]; then + PYENV_VERSION=$(head -n 1 .python-version) + export PYENV_VERSION +fi + +# other common virtualenvs +for venv in venv .venv .; do + if [ -f $venv/bin/activate ]; then + . $venv/bin/activate + fi +done + +exec "$@" diff --git a/tox.ini b/tox.ini index 7060a46b764..5527db738a6 100644 --- a/tox.ini +++ b/tox.ini @@ -45,4 +45,4 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pre-commit run --config .pre-commit-config-all.yaml mypy {posargs: --all-files} + pre-commit run mypy {posargs: --all-files} From b373c202c982f57139dffe0994a727adccf3cf13 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Feb 2020 17:08:49 +0100 Subject: [PATCH 045/378] deCONZ - Add support for new switch type (#31362) --- homeassistant/components/deconz/const.py | 2 +- homeassistant/components/deconz/light.py | 9 ++++++--- tests/components/deconz/test_light.py | 19 ++++++++++++++++--- tests/components/deconz/test_switch.py | 13 ++++++++++++- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e951e61fde7..293e0d9719c 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -47,7 +47,7 @@ DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device"] COVER_TYPES = DAMPERS + WINDOW_COVERS -POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] +POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 15d3b828741..ee22c86c44a 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -90,9 +90,12 @@ class DeconzLight(DeconzDevice, Light): """Set up light.""" super().__init__(device, gateway) - self._features = SUPPORT_BRIGHTNESS - self._features |= SUPPORT_FLASH - self._features |= SUPPORT_TRANSITION + self._features = 0 + + if self._device.brightness is not None: + self._features |= SUPPORT_BRIGHTNESS + self._features |= SUPPORT_FLASH + self._features |= SUPPORT_TRANSITION if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 8658eed3eb5..fbe3dd0bb32 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -59,6 +59,12 @@ LIGHTS = { "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, + "4": { + "name": "On off light", + "state": {"on": True, "reachable": True}, + "type": "On and Off light", + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, } @@ -91,18 +97,25 @@ async def test_lights_and_groups(hass): assert "light.light_group" in gateway.deconz_ids assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids - # 4 entities - assert len(hass.states.async_all()) == 4 + assert "light.on_off_light" in gateway.deconz_ids + + assert len(hass.states.async_all()) == 5 rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "on" assert rgb_light.attributes["brightness"] == 255 assert rgb_light.attributes["hs_color"] == (224.235, 100.0) assert rgb_light.attributes["is_deconz_group"] is False + assert rgb_light.attributes["supported_features"] == 61 tunable_white_light = hass.states.get("light.tunable_white_light") assert tunable_white_light.state == "on" assert tunable_white_light.attributes["color_temp"] == 2500 + assert tunable_white_light.attributes["supported_features"] == 2 + + on_off_light = hass.states.get("light.on_off_light") + assert on_off_light.state == "on" + assert on_off_light.attributes["supported_features"] == 0 light_group = hass.states.get("light.light_group") assert light_group.state == "on" @@ -219,7 +232,7 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 rgb_light = hass.states.get("light.rgb_light") assert rgb_light is not None diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 553e4f1f167..bb48a6243c6 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -38,6 +38,13 @@ SWITCHES = { "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:03-00", }, + "5": { + "id": "On off relay id", + "name": "On off relay", + "state": {"on": True, "reachable": True}, + "type": "On/Off light", + "uniqueid": "00:00:00:00:00:00:00:04-00", + }, } @@ -68,7 +75,8 @@ async def test_switches(hass): assert "switch.smart_plug" in gateway.deconz_ids assert "switch.warning_device" in gateway.deconz_ids assert "switch.unsupported_switch" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 4 + assert "switch.on_off_relay" in gateway.deconz_ids + assert len(hass.states.async_all()) == 5 on_off_switch = hass.states.get("switch.on_off_switch") assert on_off_switch.state == "on" @@ -79,6 +87,9 @@ async def test_switches(hass): warning_device = hass.states.get("switch.warning_device") assert warning_device.state == "on" + on_off_relay = hass.states.get("switch.on_off_relay") + assert on_off_relay.state == "on" + state_changed_event = { "t": "event", "e": "changed", From c67f53dc43d46c69ecb7e78a28c812bb17e9c156 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Feb 2020 08:14:28 -0800 Subject: [PATCH 046/378] Remove hour delay before checking for updates (#31368) * Check for updates at startup * Add 100% test coverage for update_coordinator * Address comments --- homeassistant/components/updater/__init__.py | 56 +++---- .../components/updater/binary_sensor.py | 58 +++---- homeassistant/helpers/update_coordinator.py | 28 ++-- tests/components/updater/test_init.py | 156 ++++-------------- tests/helpers/test_update_coordinator.py | 123 ++++++++++++++ 5 files changed, 223 insertions(+), 198 deletions(-) create mode 100644 tests/helpers/test_update_coordinator.py diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 42eb988ed56..826da31c5d5 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -12,11 +12,9 @@ from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol from homeassistant.const import __version__ as current_version -from homeassistant.helpers import discovery, event +from homeassistant.helpers import discovery, update_coordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -28,8 +26,6 @@ CONF_COMPONENT_REPORTING = "include_used_components" DOMAIN = "updater" -DISPATCHER_REMOTE_UPDATE = "updater_remote_update" - UPDATER_URL = "https://updater.home-assistant.io/" UPDATER_UUID_FILE = ".uuid" @@ -84,10 +80,6 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") - hass.async_create_task( - discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) - ) - config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): huuid = await hass.async_add_job(_load_uuid, hass) @@ -96,18 +88,17 @@ async def async_setup(hass, config): include_components = config.get(CONF_COMPONENT_REPORTING) - async def check_new_version(now): + async def check_new_version(): """Check if a new version is available and report if one is.""" - result = await get_newest_version(hass, huuid, include_components) + newest, release_notes = await get_newest_version( + hass, huuid, include_components + ) - if result is None: - return - - newest, release_notes = result + _LOGGER.debug("Fetched version %s: %s", newest, release_notes) # Skip on dev - if newest is None or "dev" in current_version: - return + if "dev" in current_version: + return Updater(False, "", "") # Load data from supervisor on Hass.io if hass.components.hassio.is_hassio(): @@ -116,20 +107,29 @@ async def async_setup(hass, config): # Validate version update_available = False if StrictVersion(newest) > StrictVersion(current_version): - _LOGGER.info("The latest available version of Home Assistant is %s", newest) + _LOGGER.debug( + "The latest available version of Home Assistant is %s", newest + ) update_available = True elif StrictVersion(newest) == StrictVersion(current_version): - _LOGGER.info("You are on the latest version (%s) of Home Assistant", newest) + _LOGGER.debug( + "You are on the latest version (%s) of Home Assistant", newest + ) elif StrictVersion(newest) < StrictVersion(current_version): _LOGGER.debug("Local version is newer than the latest version (%s)", newest) - updater = Updater(update_available, newest, release_notes) - async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, updater) + _LOGGER.debug("Update available: %s", update_available) - # Update daily, start 1 hour after startup - _dt = dt_util.utcnow() + timedelta(hours=1) - event.async_track_utc_time_change( - hass, check_new_version, hour=_dt.hour, minute=_dt.minute, second=_dt.second + return Updater(update_available, newest, release_notes) + + coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator( + hass, _LOGGER, "Home Assistant update", check_new_version, timedelta(days=1) + ) + + await coordinator.async_refresh() + + hass.async_create_task( + discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) ) return True @@ -164,17 +164,17 @@ async def get_newest_version(hass, huuid, include_components): ) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not contact Home Assistant Update to check for updates") - return None + raise update_coordinator.UpdateFailed try: res = await req.json() except ValueError: _LOGGER.error("Received invalid JSON from Home Assistant Update") - return None + raise update_coordinator.UpdateFailed try: res = RESPONSE_SCHEMA(res) return res["version"], res["release-notes"] except vol.Invalid: _LOGGER.error("Got unexpected response: %s", res) - return None + raise update_coordinator.UpdateFailed diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 3e026a87d4d..60f5cfedf6e 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,26 +1,24 @@ """Support for Home Assistant Updater binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater +from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the updater binary sensors.""" - async_add_entities([UpdaterBinary()]) + if discovery_info is None: + return + + async_add_entities([UpdaterBinary(hass.data[UPDATER_DOMAIN])]) class UpdaterBinary(BinarySensorDevice): """Representation of an updater binary sensor.""" - def __init__(self): + def __init__(self, coordinator): """Initialize the binary sensor.""" - self._update_available = None - self._release_notes = None - self._newest_version = None - self._unsub_dispatcher = None + self.coordinator = coordinator @property def name(self) -> str: @@ -35,12 +33,12 @@ class UpdaterBinary(BinarySensorDevice): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self._update_available + return self.coordinator.data.update_available @property def available(self) -> bool: """Return True if entity is available.""" - return self._update_available is not None + return not self.coordinator.failed_last_update @property def should_poll(self) -> bool: @@ -50,32 +48,24 @@ class UpdaterBinary(BinarySensorDevice): @property def device_state_attributes(self) -> dict: """Return the optional state attributes.""" - data = super().device_state_attributes - if data is None: - data = {} - if self._release_notes: - data[ATTR_RELEASE_NOTES] = self._release_notes - if self._newest_version: - data[ATTR_NEWEST_VERSION] = self._newest_version + data = {} + if self.coordinator.data.release_notes: + data[ATTR_RELEASE_NOTES] = self.coordinator.data.release_notes + if self.coordinator.data.newest_version: + data[ATTR_NEWEST_VERSION] = self.coordinator.data.newest_version return data async def async_added_to_hass(self): """Register update dispatcher.""" - - @callback - def async_state_update(updater: Updater): - """Update callback.""" - self._newest_version = updater.newest_version - self._release_notes = updater.release_notes - self._update_available = updater.update_available - self.async_schedule_update_ha_state() - - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update - ) + self.coordinator.async_add_listener(self.async_write_ha_state) async def async_will_remove_from_hass(self): - """Register update dispatcher.""" - if self._unsub_dispatcher is not None: - self._unsub_dispatcher() - self._unsub_dispatcher = None + """When removed from hass.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + + 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/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index dc990637e31..a882e2880b1 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -11,6 +11,9 @@ from homeassistant.util.dt import utcnow from .debounce import Debouncer +REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 +REQUEST_REFRESH_DEFAULT_IMMEDIATE = True + class UpdateFailed(Exception): """Raised when an update has failed.""" @@ -26,7 +29,7 @@ class DataUpdateCoordinator: name: str, update_method: Callable[[], Awaitable], update_interval: timedelta, - request_refresh_debouncer: Debouncer, + request_refresh_debouncer: Optional[Debouncer] = None, ): """Initialize global data updater.""" self.hass = hass @@ -41,8 +44,15 @@ class DataUpdateCoordinator: self._unsub_refresh: Optional[CALLBACK_TYPE] = None self._request_refresh_task: Optional[asyncio.TimerHandle] = None self.failed_last_update = False + if request_refresh_debouncer is None: + request_refresh_debouncer = Debouncer( + hass, + logger, + REQUEST_REFRESH_DEFAULT_COOLDOWN, + REQUEST_REFRESH_DEFAULT_IMMEDIATE, + ) self._debounced_refresh = request_refresh_debouncer - request_refresh_debouncer.function = self._async_do_refresh + request_refresh_debouncer.function = self.async_refresh @callback def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None: @@ -64,14 +74,6 @@ class DataUpdateCoordinator: self._unsub_refresh() self._unsub_refresh = None - async def async_refresh(self) -> None: - """Refresh the data.""" - if self._unsub_refresh: - self._unsub_refresh() - self._unsub_refresh = None - - await self._async_do_refresh() - @callback def _schedule_refresh(self) -> None: """Schedule a refresh.""" @@ -86,7 +88,7 @@ class DataUpdateCoordinator: async def _handle_refresh_interval(self, _now: datetime) -> None: """Handle a refresh interval occurrence.""" self._unsub_refresh = None - await self._async_do_refresh() + await self.async_refresh() async def async_request_refresh(self) -> None: """Request a refresh. @@ -95,8 +97,8 @@ class DataUpdateCoordinator: """ await self._debounced_refresh.async_call() - async def _async_do_refresh(self) -> None: - """Time to update.""" + async def async_refresh(self) -> None: + """Update data.""" if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 07b5cb059bf..10fa026db29 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -1,20 +1,15 @@ """The tests for the Updater component.""" import asyncio -from datetime import timedelta -from unittest.mock import Mock, patch +from unittest.mock import Mock +from asynctest import patch import pytest from homeassistant.components import updater +from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util -from tests.common import ( - MockDependency, - async_fire_time_changed, - mock_component, - mock_coro, -) +from tests.common import MockDependency, mock_component, mock_coro NEW_VERSION = "10000.0" MOCK_VERSION = "10.0" @@ -32,95 +27,39 @@ def mock_distro(): yield +@pytest.fixture(autouse=True) +def mock_version(): + """Mock current version.""" + with patch("homeassistant.components.updater.current_version", MOCK_VERSION): + yield + + @pytest.fixture(name="mock_get_newest_version") def mock_get_newest_version_fixture(): """Fixture to mock get_newest_version.""" - with patch("homeassistant.components.updater.get_newest_version") as mock: + with patch( + "homeassistant.components.updater.get_newest_version", + return_value=(NEW_VERSION, RELEASE_NOTES), + ) as mock: yield mock -@pytest.fixture(name="mock_get_uuid") +@pytest.fixture(name="mock_get_uuid", autouse=True) def mock_get_uuid_fixture(): """Fixture to mock get_uuid.""" with patch("homeassistant.components.updater._load_uuid") as mock: yield mock -@pytest.fixture(name="mock_utcnow") -def mock_utcnow_fixture(): - """Fixture to mock utcnow.""" - with patch("homeassistant.components.updater.dt_util") as mock: - yield mock.utcnow - - -async def test_new_version_shows_entity_startup( - hass, mock_get_uuid, mock_get_newest_version -): - """Test if binary sensor is unavailable at first.""" - mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) - - res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - assert res, "Updater failed to set up" - - await hass.async_block_till_done() - assert hass.states.is_state("binary_sensor.updater", "unavailable") - assert "newest_version" not in hass.states.get("binary_sensor.updater").attributes - assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes - - -async def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version, mock_utcnow): - """Test if renaming the binary sensor works correctly.""" - mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) - - now = dt_util.utcnow() - later = now + timedelta(hours=1) - mock_utcnow.return_value = now - - res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - assert res, "Updater failed to set up" - - await hass.async_block_till_done() - assert hass.states.is_state("binary_sensor.updater", "unavailable") - assert hass.states.get("binary_sensor.new_entity_id") is None - - entity_registry = await hass.helpers.entity_registry.async_get_registry() - entity_registry.async_update_entity( - "binary_sensor.updater", new_entity_id="binary_sensor.new_entity_id" - ) - - await hass.async_block_till_done() - assert hass.states.is_state("binary_sensor.new_entity_id", "unavailable") - assert hass.states.get("binary_sensor.updater") is None - - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, later) - await hass.async_block_till_done() - - assert hass.states.is_state("binary_sensor.new_entity_id", "on") - assert hass.states.get("binary_sensor.updater") is None - - async def test_new_version_shows_entity_true( - hass, mock_get_uuid, mock_get_newest_version, mock_utcnow + hass, mock_get_uuid, mock_get_newest_version ): """Test if sensor is true if new version is available.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) - now = dt_util.utcnow() - later = now + timedelta(hours=1) - mock_utcnow.return_value = now - - res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - assert res, "Updater failed to set up" + assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) await hass.async_block_till_done() - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, later) - await hass.async_block_till_done() - assert hass.states.is_state("binary_sensor.updater", "on") assert ( hass.states.get("binary_sensor.updater").attributes["newest_version"] @@ -133,23 +72,15 @@ async def test_new_version_shows_entity_true( async def test_same_version_shows_entity_false( - hass, mock_get_uuid, mock_get_newest_version, mock_utcnow + hass, mock_get_uuid, mock_get_newest_version ): """Test if sensor is false if no new version is available.""" mock_get_uuid.return_value = MOCK_HUUID mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) - now = dt_util.utcnow() - later = now + timedelta(hours=1) - mock_utcnow.return_value = now - - res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - assert res, "Updater failed to set up" + assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) await hass.async_block_till_done() - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, later) - await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "off") assert ( @@ -159,29 +90,18 @@ async def test_same_version_shows_entity_false( assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes -async def test_disable_reporting( - hass, mock_get_uuid, mock_get_newest_version, mock_utcnow -): +async def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): """Test we do not gather analytics when disable reporting is active.""" mock_get_uuid.return_value = MOCK_HUUID mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) - now = dt_util.utcnow() - later = now + timedelta(hours=1) - mock_utcnow.return_value = now - - res = await async_setup_component( + assert await async_setup_component( hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}} ) - assert res, "Updater failed to set up" - await hass.async_block_till_done() - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, later) - await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "off") - res = await updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG) + await updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG) call = mock_get_newest_version.mock_calls[0][1] assert call[0] is hass assert call[1] is None @@ -215,9 +135,10 @@ async def test_error_fetching_new_version_timeout(hass): with patch( "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), - ), patch("async_timeout.timeout", side_effect=asyncio.TimeoutError): - res = await updater.get_newest_version(hass, MOCK_HUUID, False) - assert res is None + ), patch("async_timeout.timeout", side_effect=asyncio.TimeoutError), pytest.raises( + UpdateFailed + ): + await updater.get_newest_version(hass, MOCK_HUUID, False) async def test_error_fetching_new_version_bad_json(hass, aioclient_mock): @@ -227,9 +148,8 @@ async def test_error_fetching_new_version_bad_json(hass, aioclient_mock): with patch( "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), - ): - res = await updater.get_newest_version(hass, MOCK_HUUID, False) - assert res is None + ), pytest.raises(UpdateFailed): + await updater.get_newest_version(hass, MOCK_HUUID, False) async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): @@ -245,31 +165,21 @@ async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock) with patch( "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), - ): - res = await updater.get_newest_version(hass, MOCK_HUUID, False) - assert res is None + ), pytest.raises(UpdateFailed): + await updater.get_newest_version(hass, MOCK_HUUID, False) async def test_new_version_shows_entity_after_hour_hassio( - hass, mock_get_uuid, mock_get_newest_version, mock_utcnow + hass, mock_get_uuid, mock_get_newest_version ): """Test if binary sensor gets updated if new version is available / Hass.io.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) mock_component(hass, "hassio") hass.data["hassio_hass_version"] = "999.0" - now = dt_util.utcnow() - later = now + timedelta(hours=1) - mock_utcnow.return_value = now - - res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - assert res, "Updater failed to set up" + assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) await hass.async_block_till_done() - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, later) - await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "on") assert ( diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py new file mode 100644 index 00000000000..8c792506833 --- /dev/null +++ b/tests/helpers/test_update_coordinator.py @@ -0,0 +1,123 @@ +"""Tests for the update coordinator.""" +from datetime import timedelta +import logging + +from asynctest import CoroutineMock, Mock +import pytest + +from homeassistant.helpers import update_coordinator +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + +LOGGER = logging.getLogger(__name__) + + +@pytest.fixture +def crd(hass): + """Coordinator mock.""" + calls = [] + + async def refresh(): + calls.append(None) + return len(calls) + + crd = update_coordinator.DataUpdateCoordinator( + hass, LOGGER, "test", refresh, timedelta(seconds=10), + ) + return crd + + +async def test_async_refresh(crd): + """Test async_refresh for update coordinator.""" + assert crd.data is None + await crd.async_refresh() + assert crd.data == 1 + assert crd.failed_last_update is False + + updates = [] + + def update_callback(): + updates.append(crd.data) + + crd.async_add_listener(update_callback) + + await crd.async_refresh() + + assert updates == [2] + + crd.async_remove_listener(update_callback) + + await crd.async_refresh() + + assert updates == [2] + + +async def test_request_refresh(crd): + """Test request refresh for update coordinator.""" + assert crd.data is None + await crd.async_request_refresh() + assert crd.data == 1 + assert crd.failed_last_update is False + + # Second time we hit the debonuce + await crd.async_request_refresh() + assert crd.data == 1 + assert crd.failed_last_update is False + + +async def test_refresh_fail(crd, caplog): + """Test a failing update function.""" + crd.update_method = CoroutineMock(side_effect=update_coordinator.UpdateFailed) + + await crd.async_refresh() + + assert crd.data is None + assert crd.failed_last_update is True + assert "Error fetching test data" in caplog.text + + crd.update_method = CoroutineMock(return_value=1) + + await crd.async_refresh() + + assert crd.data == 1 + assert crd.failed_last_update is False + + crd.update_method = CoroutineMock(side_effect=ValueError) + caplog.clear() + + await crd.async_refresh() + + assert crd.data == 1 # value from previous fetch + assert crd.failed_last_update is True + assert "Unexpected error fetching test data" in caplog.text + + +async def test_update_interval(hass, crd): + """Test update interval works.""" + # Test we don't update without subscriber + async_fire_time_changed(hass, utcnow() + crd.update_interval) + await hass.async_block_till_done() + assert crd.data is None + + # Add subscriber + update_callback = Mock() + crd.async_add_listener(update_callback) + + # Test twice we update with subscriber + async_fire_time_changed(hass, utcnow() + crd.update_interval) + await hass.async_block_till_done() + assert crd.data == 1 + + async_fire_time_changed(hass, utcnow() + crd.update_interval) + await hass.async_block_till_done() + assert crd.data == 2 + + # Test removing listener + crd.async_remove_listener(update_callback) + + async_fire_time_changed(hass, utcnow() + crd.update_interval) + await hass.async_block_till_done() + + # Test we stop updating after we lose last subscriber + assert crd.data == 2 From 3275987f171b307feda86205a061915fc6030532 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 1 Feb 2020 17:38:36 +0100 Subject: [PATCH 047/378] Add play, pause, previous and next track to kef (#31373) * KEF: add support for play, pause, next track, and previous track * bump aiokef to 0.2.7 * run isort * do not dynamically change the supported features --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 24 ++++++++++++++++++++ requirements_all.txt | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index a2769cd8eb6..135f8e1cf54 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.6", "getmac==0.8.1"] + "requirements": ["aiokef==0.2.7", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index dc91b94f5ef..d4a1d7a4df3 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -11,6 +11,10 @@ import voluptuous as vol from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -214,6 +218,10 @@ class KefMediaPlayer(MediaPlayerDevice): | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | SUPPORT_TURN_OFF + | SUPPORT_NEXT_TRACK # only in Bluetooth and Wifi + | SUPPORT_PAUSE # only in Bluetooth and Wifi + | SUPPORT_PLAY # only in Bluetooth and Wifi + | SUPPORT_PREVIOUS_TRACK # only in Bluetooth and Wifi ) if self._supports_on: support_kef |= SUPPORT_TURN_ON @@ -280,3 +288,19 @@ class KefMediaPlayer(MediaPlayerDevice): await self._speaker.set_source(source) else: raise ValueError(f"Unknown input source: {source}.") + + async def async_media_play(self): + """Send play command.""" + await self._speaker.play_pause() + + async def async_media_pause(self): + """Send pause command.""" + await self._speaker.play_pause() + + async def async_media_previous_track(self): + """Send previous track command.""" + await self._speaker.prev_track() + + async def async_media_next_track(self): + """Send next track command.""" + await self._speaker.next_track() diff --git a/requirements_all.txt b/requirements_all.txt index 87a2b0d8f1c..a0ef25566ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.6 +aiokef==0.2.7 # homeassistant.components.lifx aiolifx==0.6.7 From 9821047d7510dca86c7d686a8583a32ca53af073 Mon Sep 17 00:00:00 2001 From: dcnielsen90 Date: Sat, 1 Feb 2020 12:04:49 -0500 Subject: [PATCH 048/378] Replace unmaintained BraviaRC backend with new fork: (#31234) BraviaRC is currently unmaintained for home-assistant. This commit swaps it out with a new fork of BraviaRC called python-bravia-tv. This captures all bug fixes from BraviaRC release 3.7 (previously linked backend) to right before BraviaRC breaks when used by home-assistant. The intent of forking is to be able to continue supporting home-assistant. This is not intended to be a one off solution; this new fork will have future updates and be maintain as needed. This initial commit of python-bravia-tv improves the import process, however overall preserves the original API. Other fixes include: * Fix set-volume slider * Better error handling * Increase input options Resolves: #26351, #30964 See also: #12577, #14843, #17345, #18245 --- homeassistant/components/braviatv/manifest.json | 2 +- homeassistant/components/braviatv/media_player.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 1cb3efdd2cc..8bfa48b9195 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1"], + "requirements": ["bravia-tv==1.0", "getmac==0.8.1"], "dependencies": ["configurator"], "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index ef0640c8e87..67feb8bfc48 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -2,7 +2,7 @@ import ipaddress import logging -from braviarc.braviarc import BraviaRC +from bravia_tv import BraviaRC from getmac import get_mac_address import voluptuous as vol diff --git a/requirements_all.txt b/requirements_all.txt index a0ef25566ff..313488a4496 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -334,7 +334,7 @@ bomradarloop==0.1.3 boto3==1.9.252 # homeassistant.components.braviatv -braviarc-homeassistant==0.3.7.dev0 +bravia-tv==1.0 # homeassistant.components.broadlink broadlink==0.12.0 From e0704d73cc314fd8c134583caabdf6abe5229508 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Feb 2020 18:11:05 +0100 Subject: [PATCH 049/378] deCONZ - Services normalize bridge id (#31378) * Services should also make sure to normalize bridge id since users do not know to manage themselves --- homeassistant/components/deconz/services.py | 12 ++++++------ requirements_test_pre_commit.txt | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index f893b9880fd..f1b19c79fce 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,4 +1,5 @@ """deCONZ services.""" +from pydeconz.utils import normalize_bridge_id import voluptuous as vol from homeassistant.helpers import config_validation as cv @@ -97,15 +98,14 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - bridgeid = data.get(CONF_BRIDGE_ID) + gateway = get_master_gateway(hass) + if CONF_BRIDGE_ID in data: + gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] + field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] - gateway = get_master_gateway(hass) - if bridgeid: - gateway = hass.data[DOMAIN][bridgeid] - if entity_id: try: field = gateway.deconz_ids[entity_id] + field @@ -120,7 +120,7 @@ async def async_refresh_devices_service(hass, data): """Refresh available devices from deCONZ.""" gateway = get_master_gateway(hass) if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]] + gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] groups = set(gateway.api.groups.keys()) lights = set(gateway.api.lights.keys()) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ef10641608d..b0deb01b3da 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,6 +2,7 @@ bandit==1.6.2 black==19.10b0 +codespell==v1.16.0 flake8-docstrings==1.5.0 flake8==3.7.9 isort==v4.3.21 From 43b11f6b390484788dc3b6d45e061a19944e30ed Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Feb 2020 20:02:57 +0100 Subject: [PATCH 050/378] deCONZ - Improve config flow logging (#31381) --- homeassistant/components/deconz/config_flow.py | 12 ++++++++++++ homeassistant/components/deconz/const.py | 2 +- homeassistant/components/deconz/deconz_event.py | 4 ++-- homeassistant/components/deconz/gateway.py | 8 ++++---- homeassistant/components/deconz/services.py | 4 ++-- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 614d2378c88..3a38a67f0c6 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure deCONZ component.""" import asyncio +from pprint import pformat from urllib.parse import urlparse import async_timeout @@ -26,6 +27,7 @@ from .const import ( DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, DOMAIN, + LOGGER, ) DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" @@ -93,6 +95,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except (asyncio.TimeoutError, ResponseError): self.bridges = [] + LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges)) + if len(self.bridges) == 1: return await self.async_step_user(self.bridges[0]) @@ -121,6 +125,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Attempt to link with the deCONZ bridge.""" errors = {} + LOGGER.debug( + "Preparing linking with deCONZ gateway %s", pformat(self.deconz_config) + ) + if user_input is not None: session = aiohttp_client.async_get_clientsession(self.hass) @@ -170,6 +178,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="not_deconz_bridge") + LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) + self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) @@ -196,6 +206,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the discovery component. """ + LOGGER.debug("deCONZ HASSIO discovery %s", pformat(user_input)) + self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL]) await self.async_set_unique_id(self.bridge_id) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 293e0d9719c..11dbd07e86a 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,7 +1,7 @@ """Constants for the deCONZ component.""" import logging -_LOGGER = logging.getLogger(__package__) +LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 527e8d2ab7a..aad133f583b 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -3,7 +3,7 @@ from homeassistant.const import CONF_EVENT, CONF_ID from homeassistant.core import callback from homeassistant.util import slugify -from .const import _LOGGER, CONF_GESTURE +from .const import CONF_GESTURE, LOGGER from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" @@ -25,7 +25,7 @@ class DeconzEvent(DeconzBase): self.device_id = None self.event_id = slugify(self._device.name) - _LOGGER.debug("deCONZ event created: %s", self.event_id) + LOGGER.debug("deCONZ event created: %s", self.event_id) @property def device(self): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 04452cc313c..f33e753e600 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -19,13 +19,13 @@ from homeassistant.helpers.entity_registry import ( ) from .const import ( - _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_MASTER_GATEWAY, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, + LOGGER, NEW_DEVICE, SUPPORTED_PLATFORMS, ) @@ -103,7 +103,7 @@ class DeconzGateway: raise ConfigEntryNotReady 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) return False for component in SUPPORTED_PLATFORMS: @@ -221,11 +221,11 @@ async def get_gateway( return deconz except errors.Unauthorized: - _LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) + LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) raise AuthenticationRequired except (asyncio.TimeoutError, errors.RequestError): - _LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) + LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) raise CannotConnect diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index f1b19c79fce..c85fa8073a3 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -6,9 +6,9 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway from .const import ( - _LOGGER, CONF_BRIDGE_ID, DOMAIN, + LOGGER, NEW_GROUP, NEW_LIGHT, NEW_SCENE, @@ -110,7 +110,7 @@ async def async_configure_service(hass, data): try: field = gateway.deconz_ids[entity_id] + field except KeyError: - _LOGGER.error("Could not find the entity %s", entity_id) + LOGGER.error("Could not find the entity %s", entity_id) return await gateway.api.request("put", field, json=data) From 1edaae34c5618fe5b9154fb35b472969841706d7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Feb 2020 20:48:23 +0100 Subject: [PATCH 051/378] UniFi - Log better information than a backtrace when poor switch data is involved (#31382) --- homeassistant/components/unifi/switch.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index be6002e886e..941f4f8ab84 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -216,7 +216,15 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): @property def port(self): """Shortcut to the switch port that client is connected to.""" - return self.device.ports[self.client.sw_port] + try: + return self.device.ports[self.client.sw_port] + except TypeError: + LOGGER.warning( + "Entity %s reports faulty device %s or port %s", + self.entity_id, + self.client.sw_mac, + self.client.sw_port, + ) class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): From e8b55552a1bc1029b63be85d7d43321aab81e380 Mon Sep 17 00:00:00 2001 From: Frank van Ierland <35344891+fierland@users.noreply.github.com> Date: Sat, 1 Feb 2020 20:52:28 +0100 Subject: [PATCH 052/378] Add temperature and humidity to xiaomi miio air quality monitor (#31287) * Added new attributes to the Miio airquality monitor: - temperature - humidity * updated Docstrings * docstrings updated --- .../components/xiaomi_miio/air_quality.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 50de263fb15..110ca7cff49 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -21,6 +21,8 @@ DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" ATTR_CO2E = "carbon_dioxide_equivalent" ATTR_TVOC = "total_volatile_organic_compounds" +ATTR_TEMP = "temperature" +ATTR_HUM = "humidity" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PROP_TO_ATTR = { "carbon_dioxide_equivalent": ATTR_CO2E, "total_volatile_organic_compounds": ATTR_TVOC, + "temperature": ATTR_TEMP, + "humidity": ATTR_HUM, } @@ -91,6 +95,8 @@ class AirMonitorB1(AirQualityEntity): self._carbon_dioxide_equivalent = None self._particulate_matter_2_5 = None self._total_volatile_organic_compounds = None + self._temperature = None + self._humidity = None async def async_update(self): """Fetch state from the miio device.""" @@ -100,6 +106,8 @@ class AirMonitorB1(AirQualityEntity): self._carbon_dioxide_equivalent = state.co2e self._particulate_matter_2_5 = round(state.pm25, 1) self._total_volatile_organic_compounds = round(state.tvoc, 3) + self._temperature = round(state.temperature, 2) + self._humidity = round(state.humidity, 2) self._available = True except DeviceException as ex: self._available = False @@ -150,6 +158,16 @@ class AirMonitorB1(AirQualityEntity): """Return the total volatile organic compounds.""" return self._total_volatile_organic_compounds + @property + def temperature(self): + """Return the current temperature.""" + return self._temperature + + @property + def humidity(self): + """Return the current humidity.""" + return self._humidity + @property def device_state_attributes(self): """Return the state attributes.""" @@ -179,6 +197,8 @@ class AirMonitorS1(AirMonitorB1): self._carbon_dioxide = state.co2 self._particulate_matter_2_5 = state.pm25 self._total_volatile_organic_compounds = state.tvoc + self._temperature = state.temperature + self._humidity = state.humidity self._available = True except DeviceException as ex: self._available = False From 3aeaf3bb96ed4de616f12eb835e4e9c8bef162c5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 1 Feb 2020 22:02:39 +0100 Subject: [PATCH 053/378] Revert "Bump alarmdecoder to 1.13.9 (#30303)" (#31385) This reverts commit f11d39f8ebf281a2069dc9f01777869c59405be4. --- homeassistant/components/alarmdecoder/manifest.json | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index fd0e79cef8a..f146f6f4a7e 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,7 +2,9 @@ "domain": "alarmdecoder", "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": ["alarmdecoder==1.13.9"], + "requirements": [ + "alarmdecoder==1.13.2" + ], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 313488a4496..041c42c5158 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -208,7 +208,7 @@ airly==0.0.2 aladdin_connect==0.3 # homeassistant.components.alarmdecoder -alarmdecoder==1.13.9 +alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage alpha_vantage==2.1.2 From 29aa1463ef7475b135b088e5b578440907e6c2ab Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 1 Feb 2020 23:04:42 +0100 Subject: [PATCH 054/378] Add PlatformNotReady --- homeassistant/components/mystrom/switch.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 3a045e0391d..ca766810a3d 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "myStrom Switch" @@ -30,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): MyStromPlug(host).get_status() except exceptions.MyStromConnectionError: _LOGGER.error("No route to device: %s", host) - return + raise PlatformNotReady() add_entities([MyStromSwitch(name, host)]) @@ -46,7 +47,7 @@ class MyStromSwitch(SwitchDevice): self._resource = resource self.data = {} self.plug = MyStromPlug(self._resource) - self.update() + self._available = True @property def name(self): @@ -63,6 +64,11 @@ class MyStromSwitch(SwitchDevice): """Return the current power consumption in W.""" return round(self.data["power"], 2) + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._available + def turn_on(self, **kwargs): """Turn the switch on.""" from pymystrom import exceptions @@ -87,6 +93,8 @@ class MyStromSwitch(SwitchDevice): try: self.data = self.plug.get_status() + self._available = True except exceptions.MyStromConnectionError: self.data = {"power": 0, "relay": False} + self._available = False _LOGGER.error("No route to device: %s", self._resource) From 294c6a713fc1abcb8192ae43d2cfc29d7de9ba90 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sat, 1 Feb 2020 23:21:16 +0100 Subject: [PATCH 055/378] Support multiple attachments in signal messenger integration (#31141) * added support for multiple attachments to signal_messenger integration * updated pysignalclirestapi version in requirements_all.txt * reworked multiple attachments feature in signal_messenger integration * stay backwards compatible by both allowing the "attachment" and the "attachments" attribute. * reworked multiple attachments feature in signal_messenger integration * stay backwards compatible by both allowing the "attachment" and the "attachments" attribute. * small change in signal_messenger integration * added deprecation warning for 'attachment' attribute * small changes in signal_messenger integration * use 'warning' instead of 'warn' when logging the warning message * re-generated requirements_test_pre_commit.txt * added tests for signal_messenger integration * regenerated requirements_test_all.txt for signal_messenger integration * added more signal_messenger tests * remove signal_messenger integration files from .coveragerc --- .coveragerc | 2 - .../components/signal_messenger/manifest.json | 2 +- .../components/signal_messenger/notify.py | 22 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/signal_messenger/__init__.py | 1 + .../signal_messenger/test_notify.py | 122 ++++++++++++++++++ 7 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 tests/components/signal_messenger/__init__.py create mode 100644 tests/components/signal_messenger/test_notify.py diff --git a/.coveragerc b/.coveragerc index 892a9f9de9d..27f63c4baae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -621,8 +621,6 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py - homeassistant/components/signal_messenger/__init__.py - homeassistant/components/signal_messenger/notify.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index 98a7b4e59a6..3efa1c33e85 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/signal_messenger", "dependencies": [], "codeowners": ["@bbernhard"], - "requirements": ["pysignalclirestapi==0.1.4"] + "requirements": ["pysignalclirestapi==0.2.4"] } diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 8fbf9c70873..cee871fb17e 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -17,6 +17,7 @@ CONF_SENDER_NR = "number" CONF_RECP_NR = "recipients" CONF_SIGNAL_CLI_REST_API = "url" ATTR_FILENAME = "attachment" +ATTR_FILENAMES = "attachments" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -34,9 +35,7 @@ def get_service(hass, config, discovery_info=None): recp_nrs = config[CONF_RECP_NR] signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API] - signal_cli_rest_api = SignalCliRestApi( - signal_cli_rest_api_url, sender_nr, api_version=1 - ) + signal_cli_rest_api = SignalCliRestApi(signal_cli_rest_api_url, sender_nr) return SignalNotificationService(recp_nrs, signal_cli_rest_api) @@ -60,12 +59,21 @@ class SignalNotificationService(BaseNotificationService): data = kwargs.get(ATTR_DATA) - filename = None - if data is not None and ATTR_FILENAME in data: - filename = data[ATTR_FILENAME] + filenames = None + if data is not None: + if ATTR_FILENAMES in data: + filenames = data[ATTR_FILENAMES] + if ATTR_FILENAME in data: + _LOGGER.warning( + "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108." + ) + if filenames is None: + filenames = [data[ATTR_FILENAME]] + else: + filenames.append(data[ATTR_FILENAME]) try: - self._signal_cli_rest_api.send_message(message, self._recp_nrs, filename) + self._signal_cli_rest_api.send_message(message, self._recp_nrs, filenames) except SignalCliRestApiError as ex: _LOGGER.error("%s", ex) raise ex diff --git a/requirements_all.txt b/requirements_all.txt index 041c42c5158..91ce2bde6fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1491,7 +1491,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.signal_messenger -pysignalclirestapi==0.1.4 +pysignalclirestapi==0.2.4 # homeassistant.components.sma pysma==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62a755f5145..3aba18079f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,6 +515,9 @@ pyps4-2ndscreen==1.0.6 # homeassistant.components.qwikswitch pyqwikswitch==0.93 +# homeassistant.components.signal_messenger +pysignalclirestapi==0.2.4 + # homeassistant.components.sma pysma==0.3.5 diff --git a/tests/components/signal_messenger/__init__.py b/tests/components/signal_messenger/__init__.py new file mode 100644 index 00000000000..e3b556f6c18 --- /dev/null +++ b/tests/components/signal_messenger/__init__.py @@ -0,0 +1 @@ +"""Tests for the signal_messenger component.""" diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py new file mode 100644 index 00000000000..dbfd19795e8 --- /dev/null +++ b/tests/components/signal_messenger/test_notify.py @@ -0,0 +1,122 @@ +"""The tests for the signal_messenger platform.""" + +import os +import tempfile +import unittest +from unittest.mock import patch + +from pysignalclirestapi import SignalCliRestApi +import requests_mock + +import homeassistant.components.signal_messenger.notify as signalmessenger +from homeassistant.setup import async_setup_component + +BASE_COMPONENT = "notify" + + +async def test_signal_messenger_init(hass): + """Test that service loads successfully.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "signal_messenger", + "url": "http://127.0.0.1:8080", + "number": "+43443434343", + "recipients": ["+435565656565"], + } + } + + with patch("pysignalclirestapi.SignalCliRestApi.send_message", return_value=None): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that service loads successfully + assert hass.services.has_service(BASE_COMPONENT, "test") + + +class TestSignalMesssenger(unittest.TestCase): + """Test the signal_messenger notify.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + recipients = ["+435565656565"] + number = "+43443434343" + client = SignalCliRestApi("http://127.0.0.1:8080", number) + self._signalmessenger = signalmessenger.SignalNotificationService( + recipients, client + ) + + @requests_mock.Mocker() + def test_send_message(self, mock): + """Test send message.""" + message = "Testing Signal Messenger platform :)" + mock.register_uri( + "POST", "http://127.0.0.1:8080/v2/send", status_code=201, + ) + mock.register_uri( + "GET", + "http://127.0.0.1:8080/v1/about", + status_code=200, + json={"versions": ["v1", "v2"]}, + ) + with self.assertLogs( + "homeassistant.components.signal_messenger.notify", level="DEBUG" + ) as context: + self._signalmessenger.send_message(message) + self.assertIn("Sending signal message", context.output[0]) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 2) + + @requests_mock.Mocker() + def test_send_message_should_show_deprecation_warning(self, mock): + """Test send message.""" + message = "Testing Signal Messenger platform with attachment :)" + mock.register_uri( + "POST", "http://127.0.0.1:8080/v2/send", status_code=201, + ) + mock.register_uri( + "GET", + "http://127.0.0.1:8080/v1/about", + status_code=200, + json={"versions": ["v1", "v2"]}, + ) + with self.assertLogs( + "homeassistant.components.signal_messenger.notify", level="WARNING" + ) as context: + with tempfile.NamedTemporaryFile( + suffix=".png", prefix=os.path.basename(__file__) + ) as tf: + data = {"data": {"attachment": tf.name}} + self._signalmessenger.send_message(message, **data) + self.assertIn( + "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108.", + context.output[0], + ) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 2) + + @requests_mock.Mocker() + def test_send_message_with_attachment(self, mock): + """Test send message.""" + message = "Testing Signal Messenger platform :)" + mock.register_uri( + "POST", "http://127.0.0.1:8080/v2/send", status_code=201, + ) + mock.register_uri( + "GET", + "http://127.0.0.1:8080/v1/about", + status_code=200, + json={"versions": ["v1", "v2"]}, + ) + with self.assertLogs( + "homeassistant.components.signal_messenger.notify", level="DEBUG" + ) as context: + with tempfile.NamedTemporaryFile( + suffix=".png", prefix=os.path.basename(__file__) + ) as tf: + data = {"data": {"attachments": [tf.name]}} + self._signalmessenger.send_message(message, **data) + self.assertIn("Sending signal message", context.output[0]) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 2) From 399173e3b3920a035fb36b7c5fc8933b6ee51ffe Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 00:33:17 +0100 Subject: [PATCH 056/378] Upgrade importlib-metadata to 1.5.0 (#31390) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ce2d357f82..e646381c966 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 home-assistant-frontend==20200130.0 -importlib-metadata==1.4.0 +importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 91ce2bde6fe..c22cd77b63f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.11.28 -importlib-metadata==1.4.0 +importlib-metadata==1.5.0 jinja2>=2.10.3 PyJWT==1.7.1 cryptography==2.8 diff --git a/setup.py b/setup.py index 521b9f2678c..7f9155d9a05 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ REQUIRES = [ "attrs==19.3.0", "bcrypt==3.1.7", "certifi>=2019.11.28", - "importlib-metadata==1.4.0", + "importlib-metadata==1.5.0", "jinja2>=2.10.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. From a958418ef1b30daa847a5a80089bba24e62137dd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 2 Feb 2020 00:31:45 +0000 Subject: [PATCH 057/378] [ci skip] Translation update --- .../components/abode/.translations/ca.json | 2 +- .../components/airly/.translations/ca.json | 3 ++ .../components/almond/.translations/de.json | 4 ++ .../components/brother/.translations/ca.json | 1 + .../components/brother/.translations/de.json | 8 ++++ .../components/elgato/.translations/ca.json | 2 +- .../garmin_connect/.translations/de.json | 24 ++++++++++++ .../geonetnz_volcano/.translations/ca.json | 2 +- .../huawei_lte/.translations/ca.json | 2 +- .../components/icloud/.translations/ca.json | 2 +- .../components/linky/.translations/de.json | 1 + .../components/mikrotik/.translations/ca.json | 10 +++-- .../components/mikrotik/.translations/de.json | 35 ++++++++++++++++++ .../components/mikrotik/.translations/ko.json | 37 +++++++++++++++++++ .../mikrotik/.translations/zh-Hant.json | 37 +++++++++++++++++++ .../components/ring/.translations/ca.json | 3 +- .../components/spotify/.translations/de.json | 18 +++++++++ .../components/vizio/.translations/ca.json | 4 +- .../components/withings/.translations/de.json | 5 +++ .../components/wled/.translations/ca.json | 2 +- 20 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/garmin_connect/.translations/de.json create mode 100644 homeassistant/components/mikrotik/.translations/de.json create mode 100644 homeassistant/components/mikrotik/.translations/ko.json create mode 100644 homeassistant/components/mikrotik/.translations/zh-Hant.json create mode 100644 homeassistant/components/spotify/.translations/de.json diff --git a/homeassistant/components/abode/.translations/ca.json b/homeassistant/components/abode/.translations/ca.json index 2424fd9b5f0..7763ff04a7a 100644 --- a/homeassistant/components/abode/.translations/ca.json +++ b/homeassistant/components/abode/.translations/ca.json @@ -14,7 +14,7 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, - "title": "Introdueix la teva informaci\u00f3 d'inici de sessi\u00f3 a Abode." + "title": "Introducci\u00f3 de la informaci\u00f3 d'inici de sessi\u00f3 a Abode." } }, "title": "Abode" diff --git a/homeassistant/components/airly/.translations/ca.json b/homeassistant/components/airly/.translations/ca.json index bf50b4f23e5..4c5a7a6bd59 100644 --- a/homeassistant/components/airly/.translations/ca.json +++ b/homeassistant/components/airly/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ja est\u00e0 configurada un integraci\u00f3 Airly amb aquestes coordenades." + }, "error": { "auth": "La clau API no \u00e9s correcta.", "name_exists": "El nom ja existeix.", diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json index b4e5f168f7c..89021793f94 100644 --- a/homeassistant/components/almond/.translations/de.json +++ b/homeassistant/components/almond/.translations/de.json @@ -6,6 +6,10 @@ "missing_configuration": "Bitte \u00fcberpr\u00fcfe die Dokumentation zur Einrichtung von Almond." }, "step": { + "hassio_confirm": { + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Hass.io-Add-On hergestellt wird: {addon}?", + "title": "Almond \u00fcber das Hass.io Add-on" + }, "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" } diff --git a/homeassistant/components/brother/.translations/ca.json b/homeassistant/components/brother/.translations/ca.json index f927488e7e0..bf592396094 100644 --- a/homeassistant/components/brother/.translations/ca.json +++ b/homeassistant/components/brother/.translations/ca.json @@ -23,6 +23,7 @@ "data": { "type": "Tipus d'impressora" }, + "description": "Vols afegir la impressora Brother {model} amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", "title": "Impressora Brother descoberta" } }, diff --git a/homeassistant/components/brother/.translations/de.json b/homeassistant/components/brother/.translations/de.json index 92c8d22148f..f99681d6d7b 100644 --- a/homeassistant/components/brother/.translations/de.json +++ b/homeassistant/components/brother/.translations/de.json @@ -9,6 +9,7 @@ "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" }, + "flow_title": "Brother-Drucker: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Einrichten der Brother-Drucker-Integration. Wenn Du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/brother", "title": "Brother Drucker" + }, + "zeroconf_confirm": { + "data": { + "type": "Typ des Druckers" + }, + "description": "M\u00f6chten Sie den Brother Drucker {model} mit der Seriennummer `{serial_number}` zum Home Assistant hinzuf\u00fcgen?", + "title": "Brother-Drucker entdeckt" } }, "title": "Brother Drucker" diff --git a/homeassistant/components/elgato/.translations/ca.json b/homeassistant/components/elgato/.translations/ca.json index b717a5abade..3ba9029eb00 100644 --- a/homeassistant/components/elgato/.translations/ca.json +++ b/homeassistant/components/elgato/.translations/ca.json @@ -15,7 +15,7 @@ "port": "N\u00famero de port" }, "description": "Configura l'Elgato Key Light per integrar-lo amb Home Assistant.", - "title": "Enlla\u00e7a Elgato Key Light" + "title": "Enlla\u00e7 amb Elgato Key Light" }, "zeroconf_confirm": { "description": "Vols afegir l'Elgato Key Light amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", diff --git a/homeassistant/components/garmin_connect/.translations/de.json b/homeassistant/components/garmin_connect/.translations/de.json new file mode 100644 index 00000000000..dc1dfe5e9bd --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Konto ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "invalid_auth": "Ung\u00fcltige Authentifizierung.", + "too_many_requests": "Zu viele Anfragen, wiederholen Sie es sp\u00e4ter.", + "unknown": "Unerwarteter Fehler." + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Geben Sie Ihre Zugangsdaten ein.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ca.json b/homeassistant/components/geonetnz_volcano/.translations/ca.json index 2e595b73040..6874256e5fe 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/ca.json +++ b/homeassistant/components/geonetnz_volcano/.translations/ca.json @@ -8,7 +8,7 @@ "data": { "radius": "Radi" }, - "title": "Introdueix els detalls del filtre." + "title": "Introducci\u00f3 dels detalls del filtre." } }, "title": "GeoNet NZ Volcano" diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index 594c2e3b16d..7a3ae23e275 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -24,7 +24,7 @@ "username": "Nom d'usuari" }, "description": "Introdueix les dades d\u2019acc\u00e9s del dispositiu. El nom d\u2019usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", - "title": "Con de Huawei LTE" + "title": "Configuraci\u00f3 de Huawei LTE" } }, "title": "Huawei LTE" diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json index 33fd399d33e..aa8f8374124 100644 --- a/homeassistant/components/icloud/.translations/ca.json +++ b/homeassistant/components/icloud/.translations/ca.json @@ -22,7 +22,7 @@ "username": "Correu electr\u00f2nic" }, "description": "Introdueix les teves credencials", - "title": "credencials d'iCloud" + "title": "Credencials d'iCloud" }, "verification_code": { "data": { diff --git a/homeassistant/components/linky/.translations/de.json b/homeassistant/components/linky/.translations/de.json index 3fc13126270..cf782edfdc4 100644 --- a/homeassistant/components/linky/.translations/de.json +++ b/homeassistant/components/linky/.translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto bereits konfiguriert", "username_exists": "Konto bereits konfiguriert" }, "error": { diff --git a/homeassistant/components/mikrotik/.translations/ca.json b/homeassistant/components/mikrotik/.translations/ca.json index acb9966d15d..75a116a3f9f 100644 --- a/homeassistant/components/mikrotik/.translations/ca.json +++ b/homeassistant/components/mikrotik/.translations/ca.json @@ -15,8 +15,10 @@ "name": "Nom", "password": "Contrasenya", "port": "Port", - "username": "Nom d'usuari" - } + "username": "Nom d'usuari", + "verify_ssl": "Utilitza SSL" + }, + "title": "Configuraci\u00f3 de Mikrotik Router" } }, "title": "Mikrotik" @@ -25,7 +27,9 @@ "step": { "device_tracker": { "data": { - "arp_ping": "Activa el ping ARP" + "arp_ping": "Activa el ping ARP", + "detection_time": "Interval per considerar a casa", + "force_dhcp": "For\u00e7a l'escaneig mitjan\u00e7ant DHCP" } } } diff --git a/homeassistant/components/mikrotik/.translations/de.json b/homeassistant/components/mikrotik/.translations/de.json new file mode 100644 index 00000000000..d3328e9c305 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/de.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "name_exists": "Name vorhanden", + "wrong_credentials": "Falsche Zugangsdaten" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername", + "verify_ssl": "Verwenden Sie SSL" + }, + "title": "Richten Sie den Mikrotik Router ein" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/ko.json b/homeassistant/components/mikrotik/.translations/ko.json new file mode 100644 index 00000000000..c91fd798d64 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "SSL \uc0ac\uc6a9" + }, + "title": "Mikrotik \ub77c\uc6b0\ud130 \uc124\uc815" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP \ud551 \ud65c\uc131\ud654", + "detection_time": "\uc2a4\uce94 \uac04\uaca9", + "force_dhcp": "DHCP \ub97c \uc0ac\uc6a9\ud558\uc5ec \uac15\uc81c \uc2a4\uce94" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/zh-Hant.json b/homeassistant/components/mikrotik/.translations/zh-Hant.json new file mode 100644 index 00000000000..6913f2c91f1 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u672a\u6210\u529f", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_credentials": "\u6191\u8b49\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u4f7f\u7528 SSL" + }, + "title": "\u8a2d\u5b9a Mikrotik \u8def\u7531\u5668" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "\u958b\u555f ARP ping", + "detection_time": "\u5224\u5b9a\u5728\u5bb6\u9593\u9694", + "force_dhcp": "\u5f37\u5236\u4f7f\u7528 DHCP \u6383\u63cf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/ca.json b/homeassistant/components/ring/.translations/ca.json index 6f549f8ef28..83d4c1ec99f 100644 --- a/homeassistant/components/ring/.translations/ca.json +++ b/homeassistant/components/ring/.translations/ca.json @@ -12,7 +12,8 @@ "data": { "password": "Contrasenya", "username": "Nom d'usuari" - } + }, + "title": "Inici de sessi\u00f3 amb un compte de Ring" } }, "title": "Ring" diff --git a/homeassistant/components/spotify/.translations/de.json b/homeassistant/components/spotify/.translations/de.json new file mode 100644 index 00000000000..49670e77285 --- /dev/null +++ b/homeassistant/components/spotify/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Sie k\u00f6nnen nur ein Spotify-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + }, + "create_entry": { + "default": "Erfolgreich mit Spotify authentifiziert." + }, + "step": { + "pick_implementation": { + "title": "Authentifizierungsmethode ausw\u00e4hlen" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index 28f922f9e33..7a4ac9a7fb2 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_setup": "Aquesta entrada ja ha estat configurada." + "already_setup": "Aquesta entrada ja ha estat configurada.", + "host_exists": "Ja existeix un component Vizio configurat amb el host.", + "name_exists": "Ja existeix un component Vizio configurat amb el nom." }, "error": { "host_exists": "L'amfitri\u00f3 ja est\u00e0 configurat.", diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index a75160fcef8..067fa97ebdc 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", + "missing_configuration": "Die Withings-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation." }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." }, "step": { + "pick_implementation": { + "title": "Authentifizierungsmethode ausw\u00e4hlen" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/wled/.translations/ca.json b/homeassistant/components/wled/.translations/ca.json index 347dc576d91..cf4d1d98f6e 100644 --- a/homeassistant/components/wled/.translations/ca.json +++ b/homeassistant/components/wled/.translations/ca.json @@ -14,7 +14,7 @@ "host": "Amfitri\u00f3 o adre\u00e7a IP" }, "description": "Configura el teu WLED per integrar-lo amb Home Assistant.", - "title": "Enlla\u00e7a el teu WLED" + "title": "Enlla\u00e7 amb WLED" }, "zeroconf_confirm": { "description": "Vols afegir el WLED `{name}` a Home Assistant?", From e796de6c599f498a548d50bcd4e8069cbc01529b Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Sat, 1 Feb 2020 19:44:40 -0500 Subject: [PATCH 058/378] Filter int in fan speed_list when yielding RangeController in Alexa (#31375) * Allow for int in fan speed_list. * Test for int in fan speed_list. * prevent yielding preset for int labels. --- .../components/alexa/capabilities.py | 8 ++++++-- tests/components/alexa/test_smart_home.py | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 02ebdf785cd..eb1474aed7e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1386,12 +1386,16 @@ class AlexaRangeController(AlexaCapability): precision=1, ) for index, speed in enumerate(speed_list): - labels = [speed.replace("_", " ")] + labels = [] + if isinstance(speed, str): + labels.append(speed.replace("_", " ")) if index == 1: labels.append(AlexaGlobalCatalog.VALUE_MINIMUM) if index == max_value: labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM) - self._resource.add_preset(value=index, labels=labels) + + if len(labels) > 0: + self._resource.add_preset(value=index, labels=labels) return self._resource.serialize_capability_resources() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 588192e6c3a..ca6b1e1ccb6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -669,7 +669,7 @@ async def test_fan_range(hass): { "friendly_name": "Test fan 5", "supported_features": 1, - "speed_list": ["off", "low", "medium", "high", "turbo", "warp_speed"], + "speed_list": ["off", "low", "medium", "high", "turbo", 5, "warp_speed"], "speed": "medium", }, ) @@ -705,7 +705,7 @@ async def test_fan_range(hass): supported_range = configuration["supportedRange"] assert supported_range["minimumValue"] == 0 - assert supported_range["maximumValue"] == 5 + assert supported_range["maximumValue"] == 6 assert supported_range["precision"] == 1 presets = configuration["presets"] @@ -737,8 +737,10 @@ async def test_fan_range(hass): }, } in presets + assert {"rangeValue": 5} not in presets + assert { - "rangeValue": 5, + "rangeValue": 6, "presetResources": { "friendlyNames": [ {"@type": "text", "value": {"text": "warp speed", "locale": "en-US"}}, @@ -767,6 +769,17 @@ async def test_fan_range(hass): payload={"rangeValue": 5}, instance="fan.speed", ) + assert call.data["speed"] == 5 + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": 6}, + instance="fan.speed", + ) assert call.data["speed"] == "warp_speed" await assert_range_changes( From 83480291ce0d608b83e4a2cc2151b7f9ea24a42d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 11:16:33 +0100 Subject: [PATCH 059/378] Upgrade sendgrid to 6.1.1 (#31394) --- homeassistant/components/sendgrid/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 8a87205d4b7..900fe9252b4 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,7 +2,7 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.1.0"], + "requirements": ["sendgrid==6.1.1"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c22cd77b63f..ab6da395973 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.1.0 +sendgrid==6.1.1 # homeassistant.components.sensehat sense-hat==2.2.0 From 55a1bf383208eac4db0c9f493b2197be7333f3e6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 11:17:24 +0100 Subject: [PATCH 060/378] Upgrade holidays to 0.10.1 (#31392) --- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index e888b0c5614..21b84d87cbb 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.9.12"], + "requirements": ["holidays==0.10.1"], "dependencies": [], "codeowners": ["@fabaff"], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index ab6da395973..de1d0c00c48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -673,7 +673,7 @@ hlk-sw16==0.0.8 hole==0.5.0 # homeassistant.components.workday -holidays==0.9.12 +holidays==0.10.1 # homeassistant.components.frontend home-assistant-frontend==20200130.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3aba18079f9..fbf6834d4bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -244,7 +244,7 @@ herepy==2.0.0 hole==0.5.0 # homeassistant.components.workday -holidays==0.9.12 +holidays==0.10.1 # homeassistant.components.frontend home-assistant-frontend==20200130.0 From 704cfcf235f28307206ab74e694a5339b7945e70 Mon Sep 17 00:00:00 2001 From: FrengerH Date: Sun, 2 Feb 2020 12:54:59 +0100 Subject: [PATCH 061/378] deCONZ - Fix magic cube awake gesture (#31403) --- homeassistant/components/deconz/deconz_event.py | 2 +- tests/components/deconz/test_deconz_event.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index aad133f583b..bf32f3d0ddb 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -50,7 +50,7 @@ class DeconzEvent(DeconzBase): CONF_EVENT: self._device.state, } - if self._device.gesture: + if self._device.gesture is not None: data[CONF_GESTURE] = self._device.gesture self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 69584f630d6..349b359d9b8 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -101,7 +101,7 @@ async def test_deconz_events(hass): mock_listener = Mock() unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["4"].async_update({"state": {"gesture": 2}}) + gateway.api.sensors["4"].async_update({"state": {"gesture": 0}}) await hass.async_block_till_done() assert len(mock_listener.mock_calls) == 1 @@ -109,7 +109,7 @@ async def test_deconz_events(hass): "id": "switch_4", "unique_id": "00:00:00:00:00:00:00:04", "event": 1000, - "gesture": 2, + "gesture": 0, } unsub() From 34aed53dcd88f3ce857a650a6a2fbde7286bd3c5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 13:05:50 +0100 Subject: [PATCH 062/378] Upgrade discord.py to 1.3.1 (#31391) --- 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 a9aeea27aef..e496ad0d532 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,7 +2,7 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.2.5"], + "requirements": ["discord.py==1.3.1"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index de1d0c00c48..b500ebb21f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ directpy==0.5 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.2.5 +discord.py==1.3.1 # homeassistant.components.updater distro==1.4.0 From 48402d49dcc54cdba26e44cdfb45e95e6e3edd15 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 13:06:44 +0100 Subject: [PATCH 063/378] Upgrade praw to 6.5.1 (#31393) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 1c58366f6b5..f1687d73e04 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,7 +2,7 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==6.5.0"], + "requirements": ["praw==6.5.1"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index b500ebb21f8..a97132d9e3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1022,7 +1022,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.reddit -praw==6.5.0 +praw==6.5.1 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbf6834d4bd..350f0afe062 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ plexwebsocket==0.0.6 pmsensor==0.4 # homeassistant.components.reddit -praw==6.5.0 +praw==6.5.1 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 From 55aa341daba2bac150e8e4924c5b5c8fdd94c4e5 Mon Sep 17 00:00:00 2001 From: Arjan van Balken Date: Sun, 2 Feb 2020 15:45:05 +0100 Subject: [PATCH 064/378] Add unique_id to essent sensors (#31408) * Fix mix-up of sensor entities and their values * Prevent multiple calls for the same meter --- homeassistant/components/essent/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index b106d9d2ae6..e3ce1ccaafa 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -1,5 +1,6 @@ """Support for Essent API.""" from datetime import timedelta +from typing import Optional from pyessent import PyEssent import voluptuous as vol @@ -73,7 +74,7 @@ class EssentBase: def update(self): """Retrieve the latest meter data from Essent.""" essent = PyEssent(self._username, self._password) - eans = essent.get_EANs() + eans = set(essent.get_EANs()) for possible_meter in eans: meter_data = essent.read_meter(possible_meter, only_last_meter_reading=True) if meter_data: @@ -92,6 +93,11 @@ class EssentMeter(Entity): self._tariff = tariff self._unit = unit + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._meter}-{self._type}-{self._tariff}" + @property def name(self): """Return the name of the sensor.""" From 7127310f10db32876ecb8c4a31314b562087ee82 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 07:13:07 -0800 Subject: [PATCH 065/378] Catch device not found in device automations (#31401) --- .../components/device_automation/__init__.py | 28 ++++++++++++++++++- .../device_automation/exceptions.py | 4 +++ .../components/device_automation/test_init.py | 14 ++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 56e087f0e5f..95b3fc9fdb3 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,5 +1,6 @@ """Helpers for device automations.""" import asyncio +from functools import wraps import logging from types import ModuleType from typing import Any, List, MutableMapping @@ -14,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import IntegrationNotFound, async_get_integration -from .exceptions import InvalidDeviceAutomationConfig +from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig # mypy: allow-untyped-calls, allow-untyped-defs @@ -117,6 +118,10 @@ async def _async_get_device_automations(hass, automation_type, device_id): domains = set() automations: List[MutableMapping[str, Any]] = [] device = device_registry.async_get(device_id) + + if device is None: + raise DeviceNotFound + for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) domains.add(config_entry.domain) @@ -173,6 +178,21 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom return capabilities +def handle_device_errors(func): + """Handle device automation errors.""" + + @wraps(func) + async def with_error_handling(hass, connection, msg): + try: + await func(hass, connection, msg) + except DeviceNotFound: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" + ) + + return with_error_handling + + @websocket_api.websocket_command( { vol.Required("type"): "device_automation/action/list", @@ -180,6 +200,7 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] @@ -194,6 +215,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] @@ -208,6 +230,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] @@ -222,6 +245,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_get_action_capabilities(hass, connection, msg): """Handle request for device action capabilities.""" action = msg["action"] @@ -238,6 +262,7 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): """Handle request for device condition capabilities.""" condition = msg["condition"] @@ -254,6 +279,7 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): """Handle request for device trigger capabilities.""" trigger = msg["trigger"] diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py index 2f7c0df0187..ad92696cb94 100644 --- a/homeassistant/components/device_automation/exceptions.py +++ b/homeassistant/components/device_automation/exceptions.py @@ -4,3 +4,7 @@ from homeassistant.exceptions import HomeAssistantError class InvalidDeviceAutomationConfig(HomeAssistantError): """When device automation config is invalid.""" + + +class DeviceNotFound(HomeAssistantError): + """When referenced device not found.""" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 651d989d105..48426e2640e 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -761,3 +761,17 @@ async def test_automation_with_bad_trigger(hass, caplog): ) assert "required key not provided" in caplog.text + + +async def test_websocket_device_not_found(hass, hass_ws_client): + """Test caling command with unknown device.""" + await async_setup_component(hass, "device_automation", {}) + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": "device_automation/action/list", "device_id": "non-existing"} + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert not msg["success"] + assert msg["error"] == {"code": "not_found", "message": "Device not found"} From f68a6ae3b2d8e4bdcafe3e394fe631c2ffb2d4b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 16:28:37 +0100 Subject: [PATCH 066/378] Upgrade numpy to 1.18.1 (#31411) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7a5eb7e56df..363269bc589 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.17.4", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.18.1", "pyiqvia==0.2.1"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 3d13db3ead3..40ab3a8a7ed 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"], + "requirements": ["numpy==1.18.1", "opencv-python-headless==4.1.2.30"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e34e9644381..03a5db01a47 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.4", + "numpy==1.18.1", "protobuf==3.6.1", "pillow==6.2.1" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 2b9e7a4eccf..2026816c090 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.17.4"], + "requirements": ["numpy==1.18.1"], "dependencies": [], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index a97132d9e3d..42080d3baf3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -914,7 +914,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.4 +numpy==1.18.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 350f0afe062..1d6fb10f934 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -324,7 +324,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.4 +numpy==1.18.1 # homeassistant.components.google oauth2client==4.0.0 From 0f6e2850ab5d5887c3f3452df7da6a0394fca2d9 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 2 Feb 2020 16:29:10 +0100 Subject: [PATCH 067/378] Update pyhomematic to 0.1.64 (#31406) --- homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index c4e09c36b8e..edf07c3e4d7 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.63"], + "requirements": ["pyhomematic==0.1.64"], "dependencies": [], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/requirements_all.txt b/requirements_all.txt index 42080d3baf3..a1994edb77e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1282,7 +1282,7 @@ pyhik==0.2.5 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.63 +pyhomematic==0.1.64 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d6fb10f934..3685be094f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ pyhaversion==3.2.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.63 +pyhomematic==0.1.64 # homeassistant.components.icloud pyicloud==0.9.2 From 61a1d8e99fe30da312609c74ef35eee5fa1f82f8 Mon Sep 17 00:00:00 2001 From: akasma74 Date: Sun, 2 Feb 2020 16:23:13 +0000 Subject: [PATCH 068/378] Fix rflink commands containing equals sign (#31412) * new lib verson available * new rflink lib version * new rflink lib version --- 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 28aea1adc31..77b6413f994 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,7 +2,7 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.50"], + "requirements": ["rflink==0.0.51"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index a1994edb77e..da993a0dc16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1747,7 +1747,7 @@ restrictedpython==5.0 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.50 +rflink==0.0.51 # homeassistant.components.ring ring_doorbell==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3685be094f1..dbd562ac15c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -582,7 +582,7 @@ regenmaschine==1.5.1 restrictedpython==5.0 # homeassistant.components.rflink -rflink==0.0.50 +rflink==0.0.51 # homeassistant.components.ring ring_doorbell==0.6.0 From 8852cd0def3de0644f92df029c5aaad6b3256c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Orri?= Date: Sun, 2 Feb 2020 17:26:51 +0100 Subject: [PATCH 069/378] Add Salt Fiber Box device tracker (#30986) * Add salt component * Update files from development checklist * Use warning log level when data cannot be retrieved Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Remove empty fields from manifest.json * Remove default arguments for host and username * Bump saltbox library version * Refactor and improve error handling * Dev checklist * Fix linting errors * Check for None and return empty list * Log on debug level instead of info * More compact None checks * Remove redundant None check * Return None on exception but store as empty list * More compact syntax Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/salt/__init__.py | 1 + .../components/salt/device_tracker.py | 71 +++++++++++++++++++ homeassistant/components/salt/manifest.json | 8 +++ requirements_all.txt | 3 + 6 files changed, 85 insertions(+) create mode 100644 homeassistant/components/salt/__init__.py create mode 100644 homeassistant/components/salt/device_tracker.py create mode 100644 homeassistant/components/salt/manifest.json diff --git a/.coveragerc b/.coveragerc index 27f63c4baae..cc2402e480b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -602,6 +602,7 @@ 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/scrape/sensor.py homeassistant/components/scsgate/* diff --git a/CODEOWNERS b/CODEOWNERS index 60501876cd9..5a66f80a1d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -285,6 +285,7 @@ homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt 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/scrape/* @fabaff diff --git a/homeassistant/components/salt/__init__.py b/homeassistant/components/salt/__init__.py new file mode 100644 index 00000000000..29c371ece52 --- /dev/null +++ b/homeassistant/components/salt/__init__.py @@ -0,0 +1 @@ +"""The salt component.""" diff --git a/homeassistant/components/salt/device_tracker.py b/homeassistant/components/salt/device_tracker.py new file mode 100644 index 00000000000..7c03403622a --- /dev/null +++ b/homeassistant/components/salt/device_tracker.py @@ -0,0 +1,71 @@ +"""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 new file mode 100644 index 00000000000..019fdf9ae5f --- /dev/null +++ b/homeassistant/components/salt/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "salt", + "name": "Salt Fiber Box", + "documentation": "https://www.home-assistant.io/integrations/salt", + "requirements": ["saltbox==0.1.3"], + "dependencies": [], + "codeowners": ["@bjornorri"] +} diff --git a/requirements_all.txt b/requirements_all.txt index da993a0dc16..5847dd784fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1782,6 +1782,9 @@ 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 f701b2245a42d4c588d647d5767ab93011358b8e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Feb 2020 17:27:06 +0100 Subject: [PATCH 070/378] Upgrade alpha_vantage to 2.1.3 (#31388) --- homeassistant/components/alpha_vantage/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 33348c9d7b3..c7220d8e059 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,7 +2,7 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": ["alpha_vantage==2.1.2"], + "requirements": ["alpha_vantage==2.1.3"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5847dd784fc..74bb217d3be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -211,7 +211,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.2 +alpha_vantage==2.1.3 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 75f1e573e4fc7487bed1f6dcaa1130710ad72d3d Mon Sep 17 00:00:00 2001 From: Gerben ten Hove Date: Sun, 2 Feb 2020 17:28:36 +0100 Subject: [PATCH 071/378] Search specific train in Nederlandse Spoorwegen (#28898) * Nederlandse Spoorwegen: search for specific trip * Reformatting with Black * Resolve pylint error * Reformat with black. --- .../nederlandse_spoorwegen/sensor.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 5477aaf0e2b..7e72db57441 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -20,6 +20,7 @@ CONF_ROUTES = "routes" CONF_FROM = "from" CONF_TO = "to" CONF_VIA = "via" +CONF_TIME = "time" ICON = "mdi:train" @@ -31,6 +32,7 @@ ROUTE_SCHEMA = vol.Schema( vol.Required(CONF_FROM): cv.string, vol.Required(CONF_TO): cv.string, vol.Optional(CONF_VIA): cv.string, + vol.Optional(CONF_TIME): cv.time, } ) @@ -68,6 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): departure.get(CONF_FROM), departure.get(CONF_TO), departure.get(CONF_VIA), + departure.get(CONF_TIME), ) ) if sensors: @@ -88,13 +91,14 @@ def valid_stations(stations, given_stations): class NSDepartureSensor(Entity): """Implementation of a NS Departure Sensor.""" - def __init__(self, nsapi, name, departure, heading, via): + def __init__(self, nsapi, name, departure, heading, via, time): """Initialize the sensor.""" self._nsapi = nsapi self._name = name self._departure = departure self._via = via self._heading = heading + self._time = time self._state = None self._trips = None @@ -180,15 +184,29 @@ class NSDepartureSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the trip information.""" + + # If looking for a specific trip time, update around that trip time only. + if self._time and ( + (datetime.now() + timedelta(minutes=30)).time() < self._time + or (datetime.now() - timedelta(minutes=30)).time() > self._time + ): + self._state = None + self._trips = None + return + + # Set the search parameter to search from a specific trip time or to just search for next trip. + if self._time: + trip_time = ( + datetime.today() + .replace(hour=self._time.hour, minute=self._time.minute) + .strftime("%d-%m-%Y %H:%M") + ) + else: + trip_time = datetime.now().strftime("%d-%m-%Y %H:%M") + try: self._trips = self._nsapi.get_trips( - datetime.now().strftime("%d-%m-%Y %H:%M"), - self._departure, - self._via, - self._heading, - True, - 0, - 2, + trip_time, self._departure, self._via, self._heading, True, 0, 2, ) if self._trips: if self._trips[0].departure_time_actual is None: From f21a058f315690d352433fd4624be37a9eb694ad Mon Sep 17 00:00:00 2001 From: Jean-Paul van Ravensberg <14926452+Cloudenius@users.noreply.github.com> Date: Sun, 2 Feb 2020 17:47:48 +0100 Subject: [PATCH 072/378] Enable SUPPORT_VOLUME_STEP (#31023) --- homeassistant/components/pioneer/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 3e71b54c9fa..b834a8e6829 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -13,6 +13,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( CONF_HOST, @@ -36,6 +37,7 @@ DEFAULT_SOURCES = {} SUPPORT_PIONEER = ( SUPPORT_PAUSE | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF From 883b8f21ce74120c28890bde64f1175723be11c7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 2 Feb 2020 19:07:20 +0100 Subject: [PATCH 073/378] deCONZ - Library cleanup (#31410) * Follow library changes * Bump dependency to v70 * Fix test --- homeassistant/components/deconz/deconz_device.py | 2 +- homeassistant/components/deconz/deconz_event.py | 2 +- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_binary_sensor.py | 4 ++-- tests/components/deconz/test_climate.py | 10 +++++----- tests/components/deconz/test_cover.py | 2 +- tests/components/deconz/test_deconz_event.py | 6 +++--- tests/components/deconz/test_light.py | 2 +- tests/components/deconz/test_sensor.py | 12 ++++++------ tests/components/deconz/test_switch.py | 4 ++-- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 06756bb49f6..85fd560da1c 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -81,7 +81,7 @@ class DeconzDevice(DeconzBase, Entity): async def async_added_to_hass(self): """Subscribe to device events.""" - self._device.register_async_callback(self.async_update_callback) + self._device.register_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id self.listeners.append( async_dispatcher_connect( diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index bf32f3d0ddb..1009ae4e54c 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -21,7 +21,7 @@ class DeconzEvent(DeconzBase): """Register callback that will be used for signals.""" super().__init__(device, gateway) - self._device.register_async_callback(self.async_update_callback) + self._device.register_callback(self.async_update_callback) self.device_id = None self.event_id = slugify(self._device.name) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index ee22c86c44a..d65f9fb3ee7 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -156,7 +156,7 @@ class DeconzLight(DeconzDevice, Light): if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) - elif "IKEA" in (self._device.manufacturer or ""): + elif "IKEA" in self._device.manufacturer: data["transitiontime"] = 0 if ATTR_FLASH in kwargs: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index adac6f54493..425a44bf042 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==69" + "pydeconz==70" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 81804dfb9f6..0792e436321 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -216,7 +216,7 @@ class DeconzSensorStateTracker: """Set up tracker.""" self.sensor = sensor self.gateway = gateway - sensor.register_async_callback(self.async_update_callback) + sensor.register_callback(self.async_update_callback) @callback def close(self): diff --git a/requirements_all.txt b/requirements_all.txt index 74bb217d3be..aaeda0753ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1188,7 +1188,7 @@ pydaikin==1.6.2 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==69 +pydeconz==70 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dbd562ac15c..92d85b3e5d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.2 # homeassistant.components.deconz -pydeconz==69 +pydeconz==70 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 1dc8e61183b..c190cd82649 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -96,7 +96,7 @@ async def test_binary_sensors(hass): "id": "1", "state": {"presence": True}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() presence_sensor = hass.states.get("binary_sensor.presence_sensor") @@ -147,7 +147,7 @@ async def test_add_new_binary_sensor(hass): "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added_event) + gateway.api.event_handler(state_added_event) await hass.async_block_till_done() assert "binary_sensor.presence_sensor" in gateway.deconz_ids diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 00c03caaac7..802571cdf60 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -95,7 +95,7 @@ async def test_climate_devices(hass): "id": "1", "config": {"mode": "off"}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -109,7 +109,7 @@ async def test_climate_devices(hass): "config": {"mode": "other"}, "state": {"on": True}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -122,7 +122,7 @@ async def test_climate_devices(hass): "id": "1", "state": {"on": False}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -232,7 +232,7 @@ async def test_verify_state_update(hass): "id": "1", "state": {"on": False}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -252,7 +252,7 @@ async def test_add_new_climate_device(hass): "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added_event) + gateway.api.event_handler(state_added_event) await hass.async_block_till_done() assert "climate.thermostat" in gateway.deconz_ids diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 5242fc6326a..4bf0ec86f4a 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -74,7 +74,7 @@ async def test_cover(hass): "id": "1", "state": {"on": True}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() level_controllable_cover = hass.states.get("cover.level_controllable_cover") diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 349b359d9b8..dd3289dea23 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -70,7 +70,7 @@ async def test_deconz_events(hass): mock_listener = Mock() unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["1"].async_update({"state": {"buttonevent": 2000}}) + gateway.api.sensors["1"].update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() assert len(mock_listener.mock_calls) == 1 @@ -85,7 +85,7 @@ async def test_deconz_events(hass): mock_listener = Mock() unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["3"].async_update({"state": {"buttonevent": 2000}}) + gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() assert len(mock_listener.mock_calls) == 1 @@ -101,7 +101,7 @@ async def test_deconz_events(hass): mock_listener = Mock() unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["4"].async_update({"state": {"gesture": 0}}) + gateway.api.sensors["4"].update({"state": {"gesture": 0}}) await hass.async_block_till_done() assert len(mock_listener.mock_calls) == 1 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index fbe3dd0bb32..8990e1ff236 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -131,7 +131,7 @@ async def test_lights_and_groups(hass): "id": "1", "state": {"on": False}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() rgb_light = hass.states.get("light.rgb_light") diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 2229031fa90..3e151b55890 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -144,7 +144,7 @@ async def test_sensors(hass): "id": "1", "state": {"lightlevel": 2000}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) state_changed_event = { "t": "event", @@ -153,7 +153,7 @@ async def test_sensors(hass): "id": "4", "config": {"battery": 75}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() light_level_sensor = hass.states.get("sensor.light_level_sensor") @@ -231,7 +231,7 @@ async def test_add_new_sensor(hass): "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added_event) + gateway.api.event_handler(state_added_event) await hass.async_block_till_done() assert "sensor.light_level_sensor" in gateway.deconz_ids @@ -248,14 +248,14 @@ async def test_add_battery_later(hass): remote = gateway.api.sensors["1"] assert len(gateway.deconz_ids) == 0 assert len(gateway.events) == 1 - assert len(remote._async_callbacks) == 2 + assert len(remote._callbacks) == 2 - remote.async_update({"config": {"battery": 50}}) + remote.update({"config": {"battery": 50}}) await hass.async_block_till_done() assert len(gateway.deconz_ids) == 1 assert len(gateway.events) == 1 - assert len(remote._async_callbacks) == 2 + assert len(remote._callbacks) == 2 battery_sensor = hass.states.get("sensor.switch_1_battery_level") assert battery_sensor is not None diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index bb48a6243c6..6e151ebd47a 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -97,7 +97,7 @@ async def test_switches(hass): "id": "1", "state": {"on": False}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) state_changed_event = { "t": "event", "e": "changed", @@ -105,7 +105,7 @@ async def test_switches(hass): "id": "3", "state": {"alert": None}, } - gateway.api.async_event_handler(state_changed_event) + gateway.api.event_handler(state_changed_event) await hass.async_block_till_done() on_off_switch = hass.states.get("switch.on_off_switch") From 826433b680b74018f28e11fcce177a45d82e2b3e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 2 Feb 2020 23:48:13 +0100 Subject: [PATCH 074/378] Fix device name Google Assistant when using aliases (#31416) * Fix device name Google Assistant when using aliases * Adjust cloud tests --- homeassistant/components/google_assistant/helpers.py | 2 +- tests/components/cloud/test_client.py | 2 +- tests/components/google_assistant/__init__.py | 5 ++++- tests/components/google_assistant/test_smart_home.py | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 983f638656d..f1b7a89bffe 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -399,7 +399,7 @@ class GoogleEntity: # use aliases aliases = entity_config.get(CONF_ALIASES) if aliases: - device["name"]["nicknames"] = aliases + device["name"]["nicknames"] = [name] + aliases if self.config.is_local_sdk_active: device["otherDeviceIds"] = [{"deviceId": self.entity_id}] diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index da20afba0b1..b9e6524b62e 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -121,7 +121,7 @@ async def test_handler_google_actions(hass): device = devices[0] assert device["id"] == "switch.test" assert device["name"]["name"] == "Config name" - assert device["name"]["nicknames"] == ["Config alias"] + assert device["name"]["nicknames"] == ["Config name", "Config alias"] assert device["type"] == "action.devices.types.SWITCH" assert device["roomHint"] == "living room" diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 9ef0599d394..c0b5aa7b193 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -104,7 +104,10 @@ DEMO_DEVICES = [ }, { "id": "light.ceiling_lights", - "name": {"name": "Roof Lights", "nicknames": ["top lights", "ceiling lights"]}, + "name": { + "name": "Roof Lights", + "nicknames": ["Roof Lights", "top lights", "ceiling lights"], + }, "traits": [ "action.devices.traits.OnOff", "action.devices.traits.Brightness", diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index b3467eae326..aa073c699f8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -92,7 +92,10 @@ async def test_sync_message(hass): "devices": [ { "id": "light.demo_light", - "name": {"name": "Demo Light", "nicknames": ["Hello", "World"]}, + "name": { + "name": "Demo Light", + "nicknames": ["Demo Light", "Hello", "World"], + }, "traits": [ trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, From 96ede54a1b2ebfcaba1310a03581e78dc058dbca Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 2 Feb 2020 23:50:30 +0100 Subject: [PATCH 075/378] always call set_volume with integer values (#31418) --- homeassistant/components/webostv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 0e98bd8e703..99df9fd17ce 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -316,7 +316,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @cmd async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - tv_volume = volume * 100 + tv_volume = int(round(volume * 100)) await self._client.set_volume(tv_volume) @cmd From af105d2d61aab241a59b3a01b61e4bbdedffaea0 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 2 Feb 2020 23:52:00 +0100 Subject: [PATCH 076/378] Emulated Hue + Alexa: Fix devices not discovered and error response (#30013 & #29899) (#31413) * Revert "Emulated Hue: changed the reported fallback device-type to fix Alexa compatibility issues (#30013)" This reverts commit ddc8d9e25c0c8fd4073c0c516de9fa096cceb9bc. * Revert "Emulated Hue: updated tests (#30013)" This reverts commit 90df461e752fd6ecc1dc65bae0eba17f26a82f5f. * Emulated Hue + Alexa: changed the fallback device-type again to "Dimmable Light" (#30013) after collective debugging; fixed brightness for on/off-devices and scripts to prevent "device malfunction" response from Alexa (#29899) * Emulated Hue + Alexa: lint (#30013, #29899) --- .../components/emulated_hue/hue_api.py | 28 +++++++++---------- tests/components/emulated_hue/test_hue_api.py | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 882fafe630c..e9e7114074a 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -688,27 +688,25 @@ def entity_to_json(config, entity): retval["state"].update( {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} ) - elif ( - entity_features - & ( - SUPPORT_BRIGHTNESS - | SUPPORT_SET_POSITION - | SUPPORT_SET_SPEED - | SUPPORT_VOLUME_SET - | SUPPORT_TARGET_TEMPERATURE - ) - ) or entity.domain == script.DOMAIN: + elif entity_features & ( + SUPPORT_BRIGHTNESS + | SUPPORT_SET_POSITION + | SUPPORT_SET_SPEED + | SUPPORT_VOLUME_SET + | SUPPORT_TARGET_TEMPERATURE + ): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) else: - # On/off plug-in unit (Zigbee Device ID: 0x0000) - # Supports groups and on/off control - # Used for compatibility purposes with Alexa instead of "On/off light" - retval["type"] = "On/off plug-in unit" - retval["modelid"] = "HASS321" + # Dimmable light (Zigbee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + # Reports fixed brightness for compatibility with Alexa. + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" + retval["state"].update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) return retval diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 8b2e7157256..30b715c136b 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -238,7 +238,7 @@ async def test_light_without_brightness_supported(hass_hue, hue_client): ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True - assert light_without_brightness_json["type"] == "On/off plug-in unit" + assert light_without_brightness_json["type"] == "Dimmable light" async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): From 81dbdc6b9cb5b388785ba1675f257bf6c5821a1c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 15:01:52 -0800 Subject: [PATCH 077/378] Add dump service to MQTT integration (#31370) * Add dump service to MQTT integration * Lint --- homeassistant/components/mqtt/__init__.py | 39 +++++++++++++++++++-- homeassistant/components/mqtt/services.yaml | 11 ++++++ tests/components/mqtt/test_init.py | 25 +++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a6db90382bf..f64c643f0f4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -36,7 +36,7 @@ from homeassistant.exceptions import ( HomeAssistantError, Unauthorized, ) -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType @@ -68,6 +68,7 @@ DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_HASS_CONFIG = "mqtt_hass_config" SERVICE_PUBLISH = "publish" +SERVICE_DUMP = "dump" CONF_EMBEDDED = "embedded" @@ -651,7 +652,7 @@ async def async_setup_entry(hass, entry): if result == CONNECTION_FAILED_RECOVERABLE: raise ConfigEntryNotReady - async def async_stop_mqtt(event: Event): + async def async_stop_mqtt(_event: Event): """Stop MQTT component.""" await hass.data[DATA_MQTT].async_disconnect() @@ -683,6 +684,40 @@ async def async_setup_entry(hass, entry): DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA ) + async def async_dump_service(call: ServiceCall): + """Handle MQTT dump service calls.""" + messages = [] + + @callback + def collect_msg(msg): + messages.append((msg.topic, msg.payload.replace("\n", ""))) + + unsub = await async_subscribe(hass, call.data["topic"], collect_msg) + + def write_dump(): + with open(hass.config.path("mqtt_dump.txt"), "wt") as fp: + for msg in messages: + fp.write(",".join(msg) + "\n") + + async def finish_dump(_): + """Write dump to file.""" + unsub() + await hass.async_add_executor_job(write_dump) + + event.async_call_later(hass, call.data["duration"], finish_dump) + + hass.services.async_register( + DOMAIN, + SERVICE_DUMP, + async_dump_service, + schema=vol.Schema( + { + vol.Required("topic"): valid_subscribe_topic, + vol.Optional("duration", default=5): int, + } + ), + ) + if conf.get(CONF_DISCOVERY): await _async_setup_discovery( hass, conf, hass.data[DATA_MQTT_HASS_CONFIG], entry diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index e338e21802a..77b3e3b27a1 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -24,3 +24,14 @@ publish: description: If message should have the retain flag set. example: true default: false + +dump: + description: Dump messages on a topic selector to the 'mqtt_dump.txt' file in your config folder. + fields: + topic: + description: topic to listen to + example: "openzwave/#" + duration: + description: how long we should listen for messages in seconds + example: 5 + default: 5 diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 682aacdb746..dc79cb8a2e7 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,4 +1,5 @@ """The tests for the MQTT component.""" +from datetime import timedelta import ssl import unittest from unittest import mock @@ -16,10 +17,12 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from tests.common import ( MockConfigEntry, async_fire_mqtt_message, + async_fire_time_changed, async_mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, @@ -803,3 +806,25 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client): await client.send_json({"id": 8, "type": "unsubscribe_events", "subscription": 5}) response = await client.receive_json() assert response["success"] + + +async def test_dump_service(hass): + """Test that we can dump a topic.""" + await async_mock_mqtt_component(hass) + + mock_open = mock.mock_open() + + await hass.services.async_call( + "mqtt", "dump", {"topic": "bla/#", "duration": 3}, blocking=True + ) + async_fire_mqtt_message(hass, "bla/1", "test1") + async_fire_mqtt_message(hass, "bla/2", "test2") + + with mock.patch("homeassistant.components.mqtt.open", mock_open): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + writes = mock_open.return_value.write.mock_calls + assert len(writes) == 2 + assert writes[0][1][0] == "bla/1,test1\n" + assert writes[1][1][0] == "bla/2,test2\n" From 7687ac8b918d458d2b5785d6171cc4ec38e09d0a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 15:36:39 -0800 Subject: [PATCH 078/378] Fix service annotations (#31402) * Fix service annotations * Filter area_id from service data * Fix services not accepting entities * Typo --- .../components/input_select/__init__.py | 17 +++-- .../components/media_player/__init__.py | 65 ++++++++++++++----- homeassistant/helpers/config_validation.py | 4 +- homeassistant/helpers/service.py | 9 ++- tests/helpers/test_service.py | 12 +++- 5 files changed, 78 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 26a07e600f3..6044375d8a8 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -143,11 +143,15 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) component.async_register_entity_service( - SERVICE_SELECT_NEXT, {}, lambda entity, call: entity.async_offset_index(1) + SERVICE_SELECT_NEXT, + {}, + callback(lambda entity, call: entity.async_offset_index(1)), ) component.async_register_entity_service( - SERVICE_SELECT_PREVIOUS, {}, lambda entity, call: entity.async_offset_index(-1) + SERVICE_SELECT_PREVIOUS, + {}, + callback(lambda entity, call: entity.async_offset_index(-1)), ) component.async_register_entity_service( @@ -248,7 +252,8 @@ class InputSelect(RestoreEntity): """Return unique id for the entity.""" return self._config[CONF_ID] - async def async_select_option(self, option): + @callback + def async_select_option(self, option): """Select new option.""" if option not in self._options: _LOGGER.warning( @@ -260,14 +265,16 @@ class InputSelect(RestoreEntity): self._current_option = option self.async_write_ha_state() - async def async_offset_index(self, offset): + @callback + def async_offset_index(self, offset): """Offset current index.""" current_index = self._options.index(self._current_option) new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] self.async_write_ha_state() - async def async_set_options(self, options): + @callback + def async_set_options(self, options): """Set options.""" self._current_option = options[0] self._config[CONF_OPTIONS] = options diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 9b4c3fc1198..8a31dbe6bdb 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -173,6 +173,23 @@ SCHEMA_WEBSOCKET_GET_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.exten ) +def _rename_keys(**keys): + """Create validator that renames keys. + + Necessary because the service schema names do not match the command parameters. + + Async friendly. + """ + + def rename(value): + for to_key, from_key in keys.items(): + if from_key in value: + value[to_key] = value.pop(from_key) + return value + + return rename + + async def async_setup(hass, config): """Track states and offer events for media_players.""" component = hass.data[DOMAIN] = EntityComponent( @@ -238,30 +255,39 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_VOLUME_SET, - {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}, - lambda entity, call: entity.async_set_volume_level( - volume=call.data[ATTR_MEDIA_VOLUME_LEVEL] + vol.All( + cv.make_entity_service_schema( + {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float} + ), + _rename_keys(volume=ATTR_MEDIA_VOLUME_LEVEL), ), + "async_set_volume_level", [SUPPORT_VOLUME_SET], ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, - {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}, - lambda entity, call: entity.async_mute_volume( - mute=call.data[ATTR_MEDIA_VOLUME_MUTED] + vol.All( + cv.make_entity_service_schema( + {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean} + ), + _rename_keys(mute=ATTR_MEDIA_VOLUME_MUTED), ), + "async_mute_volume", [SUPPORT_VOLUME_MUTE], ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, - { - vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( - vol.Coerce(float), vol.Range(min=0) - ) - }, - lambda entity, call: entity.async_media_seek( - position=call.data[ATTR_MEDIA_SEEK_POSITION] + vol.All( + cv.make_entity_service_schema( + { + vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( + vol.Coerce(float), vol.Range(min=0) + ) + } + ), + _rename_keys(position=ATTR_MEDIA_SEEK_POSITION), ), + "async_media_seek", [SUPPORT_SEEK], ) component.async_register_entity_service( @@ -278,12 +304,15 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_PLAY_MEDIA, - MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, - lambda entity, call: entity.async_play_media( - media_type=call.data[ATTR_MEDIA_CONTENT_TYPE], - media_id=call.data[ATTR_MEDIA_CONTENT_ID], - enqueue=call.data.get(ATTR_MEDIA_ENQUEUE), + vol.All( + cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), + _rename_keys( + media_type=ATTR_MEDIA_CONTENT_TYPE, + media_id=ATTR_MEDIA_CONTENT_ID, + enqueue=ATTR_MEDIA_ENQUEUE, + ), ), + "async_play_media", [SUPPORT_PLAY_MEDIA], ) component.async_register_entity_service( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e357a2ba622..852948220de 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -724,6 +724,8 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +ENTITY_SERVICE_FIELDS = (ATTR_ENTITY_ID, ATTR_AREA_ID) + def make_entity_service_schema( schema: dict, *, extra: int = vol.PREVENT_EXTRA @@ -738,7 +740,7 @@ def make_entity_service_schema( }, extra=extra, ), - has_at_least_one_key(ATTR_ENTITY_ID, ATTR_AREA_ID), + has_at_least_one_key(*ENTITY_SERVICE_FIELDS), ) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 36bfd9c8cb0..b30cab3fbd4 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -283,7 +283,11 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # If the service function is a string, we'll pass it the service call data if isinstance(func, str): - data = {key: val for key, val in call.data.items() if key != ATTR_ENTITY_ID} + data = { + key: val + for key, val in call.data.items() + if key not in cv.ENTITY_SERVICE_FIELDS + } # If the service function is not a string, we pass the service call else: data = call @@ -323,6 +327,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non for platform in platforms: platform_entities = [] for entity in platform.entities.values(): + if entity.entity_id not in entity_ids: continue @@ -380,7 +385,7 @@ async def _handle_service_platform_call( if asyncio.iscoroutine(result): _LOGGER.error( - "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to component author.", + "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", func, entity.entity_id, ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 55c2c67cee2..5585437867c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -320,14 +320,20 @@ async def test_call_with_sync_func(hass, mock_entities): async def test_call_with_sync_attr(hass, mock_entities): """Test invoking sync service calls.""" - mock_entities["light.kitchen"].sync_method = Mock() + mock_method = mock_entities["light.kitchen"].sync_method = Mock() await service.entity_service_call( hass, [Mock(entities=mock_entities)], "sync_method", - ha.ServiceCall("test_domain", "test_service", {"entity_id": "light.kitchen"}), + ha.ServiceCall( + "test_domain", + "test_service", + {"entity_id": "light.kitchen", "area_id": "abcd"}, + ), ) - assert mock_entities["light.kitchen"].sync_method.call_count == 1 + assert mock_method.call_count == 1 + # We pass empty kwargs because both entity_id and area_id are filtered out + assert mock_method.mock_calls[0][2] == {} async def test_call_context_user_not_exist(hass): From 261041550131e9b0aca1d0ebc685fb8c72dbe352 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 2 Feb 2020 17:16:09 -0700 Subject: [PATCH 079/378] Streamline SimpliSafe data and token management (#31324) * Streamline SimpliSafe API usage * Streamline SimpliSafe data and token management * Correctly define self.systems * Inline update method --- .../components/simplisafe/__init__.py | 111 ++++++++++-------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b3d3baff16f..7cd1fd1bb2d 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -176,9 +176,8 @@ async def async_setup_entry(hass, config_entry): _async_save_refresh_token(hass, config_entry, api.refresh_token) - systems = await api.get_systems() - simplisafe = SimpliSafe(hass, api, systems, config_entry) - await simplisafe.async_update() + simplisafe = SimpliSafe(hass, api, config_entry) + await simplisafe.async_init() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe for component in ("alarm_control_panel", "lock"): @@ -186,22 +185,6 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_forward_entry_setup(config_entry, component) ) - async def refresh(event_time): - """Refresh data from the SimpliSafe account.""" - await simplisafe.async_update() - _LOGGER.debug("Updated data for all SimpliSafe systems") - async_dispatcher_send(hass, TOPIC_UPDATE) - - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( - hass, refresh, DEFAULT_SCAN_INTERVAL - ) - - # Register the base station for each system: - for system in systems.values(): - hass.async_create_task( - async_register_base_station(hass, system, config_entry.entry_id) - ) - @callback def verify_system_exists(coro): """Log an error if a service call uses an invalid system ID.""" @@ -209,7 +192,7 @@ async def async_setup_entry(hass, config_entry): async def decorator(call): """Decorate.""" system_id = int(call.data[ATTR_SYSTEM_ID]) - if system_id not in systems: + if system_id not in simplisafe.systems: _LOGGER.error("Unknown system ID in service call: %s", system_id) return await coro(call) @@ -222,7 +205,7 @@ async def async_setup_entry(hass, config_entry): async def decorator(call): """Decorate.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] + system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])] if system.version != 3: _LOGGER.error("Service only available on V3 systems") return @@ -234,7 +217,7 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def remove_pin(call): """Remove a PIN.""" - system = systems[call.data[ATTR_SYSTEM_ID]] + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) except SimplipyError as err: @@ -245,7 +228,7 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def set_pin(call): """Set a PIN.""" - system = systems[call.data[ATTR_SYSTEM_ID]] + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) except SimplipyError as err: @@ -257,7 +240,7 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def set_system_properties(call): """Set one or more system parameters.""" - system = systems[call.data[ATTR_SYSTEM_ID]] + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.set_properties( { @@ -303,29 +286,58 @@ async def async_unload_entry(hass, entry): class SimpliSafe: """Define a SimpliSafe API object.""" - def __init__(self, hass, api, systems, config_entry): + def __init__(self, hass, api, config_entry): """Initialize.""" self._api = api self._config_entry = config_entry self._emergency_refresh_token_used = False self._hass = hass self.last_event_data = {} - self.systems = systems + self.systems = None - async def _update_system(self, system): - """Update a system.""" - try: + async def async_init(self): + """Initialize the data class.""" + self.systems = await self._api.get_systems() + + # Register the base station for each system: + for system in self.systems.values(): + self._hass.async_create_task( + async_register_base_station( + self._hass, system, self._config_entry.entry_id + ) + ) + + async def refresh(event_time): + """Refresh data from the SimpliSafe account.""" + await self.async_update() + + self._hass.data[DOMAIN][DATA_LISTENER][ + self._config_entry.entry_id + ] = async_track_time_interval(self._hass, refresh, DEFAULT_SCAN_INTERVAL) + + await self.async_update() + + async def async_update(self): + """Get updated data from SimpliSafe.""" + + async def update_system(system): + """Update a system.""" await system.update() + self.last_event_data[system.system_id] = await system.get_latest_event() + + tasks = [update_system(system) for system in self.systems.values()] + + def cancel_tasks(): + """Cancel tasks and ensure their cancellation is processed.""" + for task in tasks: + task.cancel() + + try: + await asyncio.gather(*tasks) except InvalidCredentialsError: - # SimpliSafe's cloud is a little shaky. At times, a 500 or 502 will - # seemingly harm simplisafe-python's existing access token _and_ refresh - # token, thus preventing the integration from recovering. However, the - # refresh token stored in the config entry escapes unscathed (again, - # apparently); so, if we detect that we're in such a situation, try a last- - # ditch effort by re-authenticating with the stored token: + cancel_tasks() + if self._emergency_refresh_token_used: - # If we've already tried this, log the error, suggest a HASS restart, - # and stop the time tracker: _LOGGER.error( "SimpliSafe authentication disconnected. Please restart HASS." ) @@ -341,31 +353,26 @@ class SimpliSafe: self._config_entry.data[CONF_TOKEN] ) except SimplipyError as err: - _LOGGER.error( - 'SimpliSafe error while updating "%s": %s', system.address, err - ) + cancel_tasks() + _LOGGER.error("SimpliSafe error while updating: %s", err) return except Exception as err: # pylint: disable=broad-except - _LOGGER.error('Unknown error while updating "%s": %s', system.address, err) + cancel_tasks() + _LOGGER.error("Unknown error while updating: %s", err) return - self.last_event_data[system.system_id] = await system.get_latest_event() + if self._api.refresh_token_dirty: + _async_save_refresh_token( + self._hass, self._config_entry, self._api.refresh_token + ) # If we've reached this point using an emergency refresh token, we're in the # clear and we can discard it: if self._emergency_refresh_token_used: self._emergency_refresh_token_used = False - async def async_update(self): - """Get updated data from SimpliSafe.""" - tasks = [self._update_system(system) for system in self.systems.values()] - - await asyncio.gather(*tasks) - - if self._api.refresh_token_dirty: - _async_save_refresh_token( - self._hass, self._config_entry, self._api.refresh_token - ) + _LOGGER.debug("Updated data for all SimpliSafe systems") + async_dispatcher_send(self._hass, TOPIC_UPDATE) class SimpliSafeEntity(Entity): From 787faaa508cf83ca4f850ee7000ad17eade915aa Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 3 Feb 2020 00:31:45 +0000 Subject: [PATCH 080/378] [ci skip] Translation update --- .../components/almond/.translations/ca.json | 1 + .../garmin_connect/.translations/it.json | 24 ++++++++++++ .../components/linky/.translations/it.json | 1 + .../components/mikrotik/.translations/da.json | 2 +- .../components/mikrotik/.translations/it.json | 37 +++++++++++++++++++ .../components/ring/.translations/ca.json | 6 +++ .../samsungtv/.translations/ca.json | 2 + .../components/vizio/.translations/ca.json | 16 ++++++-- 8 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/garmin_connect/.translations/it.json create mode 100644 homeassistant/components/mikrotik/.translations/it.json diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json index 6f7df114774..5cedcfef481 100644 --- a/homeassistant/components/almond/.translations/ca.json +++ b/homeassistant/components/almond/.translations/ca.json @@ -7,6 +7,7 @@ }, "step": { "hassio_confirm": { + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement de Hass.io: {addon}?", "title": "Almond (complement de Hass.io)" }, "pick_implementation": { diff --git a/homeassistant/components/garmin_connect/.translations/it.json b/homeassistant/components/garmin_connect/.translations/it.json new file mode 100644 index 00000000000..2d942bbc6a2 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Questo account \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare.", + "invalid_auth": "Autenticazione non valida.", + "too_many_requests": "Troppe richieste, riprovare pi\u00f9 tardi.", + "unknown": "Errore imprevisto." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci le tue credenziali", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/it.json b/homeassistant/components/linky/.translations/it.json index 09d5f7e2d2b..15f15bf8b8e 100644 --- a/homeassistant/components/linky/.translations/it.json +++ b/homeassistant/components/linky/.translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account gi\u00e0 configurato", "username_exists": "Account gi\u00e0 configurato" }, "error": { diff --git a/homeassistant/components/mikrotik/.translations/da.json b/homeassistant/components/mikrotik/.translations/da.json index edaa47e52bb..35e3cd5a08a 100644 --- a/homeassistant/components/mikrotik/.translations/da.json +++ b/homeassistant/components/mikrotik/.translations/da.json @@ -28,7 +28,7 @@ "device_tracker": { "data": { "arp_ping": "Aktiver ARP-ping", - "detection_time": "Betragt som hjemme-interval", + "detection_time": "'Betragt som hjemme'-interval", "force_dhcp": "Gennemtving scanning ved hj\u00e6lp af DHCP" } } diff --git a/homeassistant/components/mikrotik/.translations/it.json b/homeassistant/components/mikrotik/.translations/it.json new file mode 100644 index 00000000000..9bc10220a9b --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/it.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Connessione Non Riuscita", + "name_exists": "Il Nome esiste gi\u00e0", + "wrong_credentials": "Credenziali Errate" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "username": "Nome utente", + "verify_ssl": "Usa SSL" + }, + "title": "Configurare il router Mikrotik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Attivare il ping ARP", + "detection_time": "Considerare l'intervallo di casa", + "force_dhcp": "Scansione forzata con DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/ca.json b/homeassistant/components/ring/.translations/ca.json index 83d4c1ec99f..c25bdb22eee 100644 --- a/homeassistant/components/ring/.translations/ca.json +++ b/homeassistant/components/ring/.translations/ca.json @@ -8,6 +8,12 @@ "unknown": "Error inesperat" }, "step": { + "2fa": { + "data": { + "2fa": "Codi de dos factors" + }, + "title": "Autenticaci\u00f3 de dos factors" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json index 78581987df8..d938373797e 100644 --- a/homeassistant/components/samsungtv/.translations/ca.json +++ b/homeassistant/components/samsungtv/.translations/ca.json @@ -9,6 +9,7 @@ }, "step": { "confirm": { + "description": "Vols configurar la Samsung TV {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la TV demanant autenticaci\u00f3. Les configuracuons manuals d'aquesta TV es sobreescriuran.", "title": "Samsung TV" }, "user": { @@ -16,6 +17,7 @@ "host": "Amfitri\u00f3 o adre\u00e7a IP", "name": "Nom" }, + "description": "Introdeix les dades de la Samsung TV. Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent demanant autenticaci\u00f3.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index 7a4ac9a7fb2..6901ffc1736 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -1,13 +1,19 @@ { "config": { "abort": { + "already_in_progress": "El flux de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.", "already_setup": "Aquesta entrada ja ha estat configurada.", + "already_setup_with_diff_host_and_name": "Sembla que aquesta entrada ja s'ha configurat amb un amfitri\u00f3 i nom diferents a partir del n\u00famero de s\u00e8rie. Elimina les entrades antigues de configuraction.yaml i del men\u00fa d'integracions abans de provar d'afegir el dispositiu novament.", "host_exists": "Ja existeix un component Vizio configurat amb el host.", - "name_exists": "Ja existeix un component Vizio configurat amb el nom." + "name_exists": "Ja existeix un component Vizio configurat amb el nom.", + "updated_options": "Aquesta entrada ja s'ha configurat per\u00f2 les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", + "updated_volume_step": "Aquesta entrada ja s'ha configurat per\u00f2 la mida de l'increment de volum definit a la configuraci\u00f3 no coincideix, en conseq\u00fc\u00e8ncia, s'ha actualitzat." }, "error": { + "cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d\u2019intentar tornar a presentar.", "host_exists": "L'amfitri\u00f3 ja est\u00e0 configurat.", - "name_exists": "El nom ja est\u00e0 configurat." + "name_exists": "El nom ja est\u00e0 configurat.", + "tv_needs_token": "Si el tipus de dispositiu \u00e9s 'tv', cal un testimoni d'acc\u00e9s v\u00e0lid (token)." }, "step": { "user": { @@ -28,8 +34,10 @@ "data": { "timeout": "Temps d'espera de les sol\u00b7licituds API (en segons)", "volume_step": "Mida del pas de volum" - } + }, + "title": "Actualitzaci\u00f3 de les opcions de Vizo SmartCast" } - } + }, + "title": "Actualitzaci\u00f3 de les opcions de Vizo SmartCast" } } \ No newline at end of file From 744ae82933a7e4d34582ffccfd963d24dbbf1308 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 3 Feb 2020 05:28:52 +0100 Subject: [PATCH 081/378] Replace cmp option with eq and order (#31423) --- homeassistant/auth/models.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 08f2f375b41..8b4e6355700 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -31,22 +31,28 @@ class User: """A user.""" name = attr.ib(type=Optional[str]) - perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False) + perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, eq=False, order=False) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) - groups = attr.ib(type=List[Group], factory=list, cmp=False) + groups = attr.ib(type=List[Group], factory=list, eq=False, order=False) # List of credentials of a user. - credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False) + credentials = attr.ib(type=List["Credentials"], factory=list, eq=False, order=False) # Tokens associated with a user. - refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False) + refresh_tokens = attr.ib( + type=Dict[str, "RefreshToken"], factory=dict, eq=False, order=False + ) _permissions = attr.ib( - type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None + type=Optional[perm_mdl.PolicyPermissions], + init=False, + eq=False, + order=False, + default=None, ) @property From ee927fbc9ecbf06817f7844b69cc8a2da84adb52 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 3 Feb 2020 02:41:58 -0500 Subject: [PATCH 082/378] Bump pyvizio version and add additional device info (#31417) * bump pyvizio version and add additional device info * add patches for get_model and get_version * change keywrod argument to positional for _test_service --- homeassistant/components/vizio/manifest.json | 2 +- .../components/vizio/media_player.py | 16 ++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vizio/conftest.py | 15 +++++++++----- tests/components/vizio/const.py | 2 ++ tests/components/vizio/test_media_player.py | 20 +++++++++---------- 7 files changed, 38 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 7f397a4ed0c..6a5b9c33111 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.16"], + "requirements": ["pyvizio==0.1.19"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 439a9a972d4..349373017da 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -107,9 +107,17 @@ class VizioDevice(MediaPlayerDevice): self._max_volume = float(self._device.get_max_volume()) self._icon = ICON[device_class] self._available = True + self._model = None + self._sw_version = None async def async_update(self) -> None: """Retrieve latest state of the device.""" + if not self._model: + self._model = await self._device.get_model() + + if not self._sw_version: + self._sw_version = await self._device.get_version() + is_on = await self._device.get_power_state(log_api_exception=False) if is_on is None: @@ -141,9 +149,9 @@ class VizioDevice(MediaPlayerDevice): input_ = await self._device.get_current_input(log_api_exception=False) if input_ is not None: - self._current_input = input_.meta_name + self._current_input = input_ - inputs = await self._device.get_inputs(log_api_exception=False) + inputs = await self._device.get_inputs_list(log_api_exception=False) if inputs is not None: self._available_inputs = [input_.name for input_ in inputs] @@ -235,6 +243,8 @@ class VizioDevice(MediaPlayerDevice): "identifiers": {(DOMAIN, self._config_entry.unique_id)}, "name": self.name, "manufacturer": "VIZIO", + "model": self._model, + "sw_version": self._sw_version, } @property @@ -267,7 +277,7 @@ class VizioDevice(MediaPlayerDevice): async def async_select_source(self, source: str) -> None: """Select input source.""" - await self._device.input_switch(source) + await self._device.set_input(source) async def async_volume_up(self) -> None: """Increase volume of the device.""" diff --git a/requirements_all.txt b/requirements_all.txt index aaeda0753ce..92391040774 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.16 +pyvizio==0.1.19 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92d85b3e5d8..4f0e6a21a2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.16 +pyvizio==0.1.19 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index f427e6d3e5a..581ea7cdd5c 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,10 +1,9 @@ """Configure py.test.""" from asynctest import patch import pytest -from pyvizio.const import DEVICE_CLASS_SPEAKER -from pyvizio.vizio import MAX_VOLUME +from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME -from .const import CURRENT_INPUT, INPUT_LIST, UNIQUE_ID +from .const import CURRENT_INPUT, INPUT_LIST, MODEL, UNIQUE_ID, VERSION class MockInput: @@ -91,12 +90,18 @@ def vizio_update_fixture(): return_value=int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2), ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", - return_value=MockInput(CURRENT_INPUT), + return_value=CURRENT_INPUT, ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_inputs", + "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", return_value=get_mock_inputs(INPUT_LIST), ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", return_value=True, + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_model", + return_value=MODEL, + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_version", + return_value=VERSION, ): yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index dd6ecda55d0..537db445a85 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -26,6 +26,8 @@ HOST2 = "192.168.1.2:9000" ACCESS_TOKEN = "deadbeef" VOLUME_STEP = 2 UNIQUE_ID = "testid" +MODEL = "model" +VERSION = "version" MOCK_USER_VALID_TV_CONFIG = { CONF_NAME: NAME, diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index d87b86f8642..bbbbca8c359 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -7,8 +7,8 @@ import pytest from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, + MAX_VOLUME, ) -from pyvizio.vizio import MAX_VOLUME from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, @@ -111,7 +111,7 @@ async def _test_service( hass: HomeAssistantType, vizio_func_name: str, ha_service_name: str, - additional_service_data: dict = None, + additional_service_data: dict, *args, **kwargs, ) -> None: @@ -194,8 +194,8 @@ async def test_services( """Test all Vizio media player entity services.""" await _test_setup(hass, DEVICE_CLASS_TV, True) - await _test_service(hass, "pow_on", SERVICE_TURN_ON) - await _test_service(hass, "pow_off", SERVICE_TURN_OFF) + await _test_service(hass, "pow_on", SERVICE_TURN_ON, None) + await _test_service(hass, "pow_off", SERVICE_TURN_OFF, None) await _test_service( hass, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} ) @@ -203,18 +203,18 @@ async def test_services( hass, "mute_off", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False} ) await _test_service( - hass, "input_switch", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB" + hass, "set_input", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB" ) - await _test_service(hass, "vol_up", SERVICE_VOLUME_UP) - await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN) + await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None) + await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN, None) await _test_service( hass, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} ) await _test_service( hass, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} ) - await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK) - await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK) + await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) + await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) async def test_options_update( @@ -231,7 +231,7 @@ async def test_options_update( entry=config_entry, options=new_options, ) assert config_entry.options == updated_options - await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, num=VOLUME_STEP) + await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP) async def _test_update_availability_switch( From e78378d90f7e227b90bb17d65ecf047a66b9959c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Feb 2020 00:51:27 -0800 Subject: [PATCH 083/378] Pass correct config to updater (#31428) --- homeassistant/components/updater/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 826da31c5d5..0f388964bb0 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -80,13 +80,13 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") - config = config.get(DOMAIN, {}) - if config.get(CONF_REPORTING): + conf = config.get(DOMAIN, {}) + if conf.get(CONF_REPORTING): huuid = await hass.async_add_job(_load_uuid, hass) else: huuid = None - include_components = config.get(CONF_COMPONENT_REPORTING) + include_components = conf.get(CONF_COMPONENT_REPORTING) async def check_new_version(): """Check if a new version is available and report if one is.""" From f49a39218851671282a7813baf64a1e69b4cc699 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 3 Feb 2020 13:30:44 +0100 Subject: [PATCH 084/378] Add guard clause for discovery_info to tahoma platforms (#31434) --- homeassistant/components/tahoma/binary_sensor.py | 2 ++ homeassistant/components/tahoma/cover.py | 2 ++ homeassistant/components/tahoma/scene.py | 2 ++ homeassistant/components/tahoma/sensor.py | 2 ++ homeassistant/components/tahoma/switch.py | 2 ++ 5 files changed, 10 insertions(+) diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index 81078ab480b..7621a542838 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -14,6 +14,8 @@ SCAN_INTERVAL = timedelta(seconds=120) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma controller devices.""" + if discovery_info is None: + return _LOGGER.debug("Setup Tahoma Binary sensor platform") controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index fb2bedc746c..7692e9bedf7 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -51,6 +51,8 @@ TAHOMA_DEVICE_CLASSES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tahoma covers.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for device in hass.data[TAHOMA_DOMAIN]["devices"]["cover"]: diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py index e54ff91a0f6..c60f245fc50 100644 --- a/homeassistant/components/tahoma/scene.py +++ b/homeassistant/components/tahoma/scene.py @@ -10,6 +10,8 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tahoma scenes.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] scenes = [] for scene in hass.data[TAHOMA_DOMAIN]["scenes"]: diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 85ccb55761d..fb8c61607c7 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -16,6 +16,8 @@ ATTR_RSSI_LEVEL = "rssi_level" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma controller devices.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for device in hass.data[TAHOMA_DOMAIN]["devices"]["sensor"]: diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index 1612120f313..9f98e711ac9 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -13,6 +13,8 @@ ATTR_RSSI_LEVEL = "rssi_level" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma switches.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for switch in hass.data[TAHOMA_DOMAIN]["devices"]["switch"]: From 45c997ea04a3f6910a328b465e9dde2c9a29ca17 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Mon, 3 Feb 2020 13:39:45 +0000 Subject: [PATCH 085/378] Update NSAPI to 3.0.2 (#30971) * Bump NSAPI version to 3.0.1 * Compatibility with NSAPI 3.0.1 response * Removed commented code * Obsolete setups receive an upgrade notification * Bump NS-API to 3.0.2 * Assign platform values directly * Removed obsolete config warning * Improved reference to obsolete password --- .../nederlandse_spoorwegen/manifest.json | 2 +- .../nederlandse_spoorwegen/sensor.py | 55 ++++++++++++------- requirements_all.txt | 2 +- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 92231bd460c..8718843d73d 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -2,7 +2,7 @@ "domain": "nederlandse_spoorwegen", "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", - "requirements": ["nsapi==3.0.0"], + "requirements": ["nsapi==3.0.2"], "dependencies": [], "codeowners": ["@YarmoM"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 7e72db57441..74b94421e3d 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): requests.exceptions.ConnectionError, requests.exceptions.HTTPError, ) as error: - _LOGGER.error("Couldn't fetch stations, API password correct?: %s", error) + _LOGGER.error("Couldn't fetch stations, API key correct?: %s", error) return sensors = [] @@ -131,20 +131,16 @@ class NSDepartureSensor(Entity): # Static attributes attributes = { "going": self._trips[0].going, - "departure_time_planned": self._trips[0].departure_time_planned.strftime( - "%H:%M" - ), + "departure_time_planned": None, "departure_time_actual": None, "departure_delay": False, "departure_platform_planned": self._trips[0].departure_platform_planned, - "departure_platform_actual": None, - "arrival_time_planned": self._trips[0].arrival_time_planned.strftime( - "%H:%M" - ), + "departure_platform_actual": self._trips[0].departure_platform_actual, + "arrival_time_planned": None, "arrival_time_actual": None, "arrival_delay": False, - "arrival_platform_platform": self._trips[0].arrival_platform_planned, - "arrival_platform_actual": None, + "arrival_platform_planned": self._trips[0].arrival_platform_planned, + "arrival_platform_actual": self._trips[0].arrival_platform_actual, "next": None, "status": self._trips[0].status.lower(), "transfers": self._trips[0].nr_transfers, @@ -153,25 +149,46 @@ class NSDepartureSensor(Entity): ATTR_ATTRIBUTION: ATTRIBUTION, } - # Departure attributes + # Planned departure attributes + if self._trips[0].departure_time_planned is not None: + attributes["departure_time_planned"] = self._trips[ + 0 + ].departure_time_planned.strftime("%H:%M") + + # Actual departure attributes if self._trips[0].departure_time_actual is not None: attributes["departure_time_actual"] = self._trips[ 0 ].departure_time_actual.strftime("%H:%M") - attributes["departure_delay"] = True - attributes["departure_platform_actual"] = self._trips[ - 0 - ].departure_platform_actual - # Arrival attributes + # Delay departure attributes + if ( + attributes["departure_time_planned"] + and attributes["departure_time_actual"] + and attributes["departure_time_planned"] + != attributes["departure_time_actual"] + ): + attributes["departure_delay"] = True + + # Planned arrival attributes + if self._trips[0].arrival_time_planned is not None: + attributes["arrival_time_planned"] = self._trips[ + 0 + ].arrival_time_planned.strftime("%H:%M") + + # Actual arrival attributes if self._trips[0].arrival_time_actual is not None: attributes["arrival_time_actual"] = self._trips[ 0 ].arrival_time_actual.strftime("%H:%M") + + # Delay arrival attributes + if ( + attributes["arrival_time_planned"] + and attributes["arrival_time_actual"] + and attributes["arrival_time_planned"] != attributes["arrival_time_actual"] + ): attributes["arrival_delay"] = True - attributes["arrival_platform_actual"] = self._trips[ - 0 - ].arrival_platform_actual # Next attributes if self._trips[1].departure_time_actual is not None: diff --git a/requirements_all.txt b/requirements_all.txt index 92391040774..cd6c0647f46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -902,7 +902,7 @@ niko-home-control==0.2.1 niluclient==0.1.2 # homeassistant.components.nederlandse_spoorwegen -nsapi==3.0.0 +nsapi==3.0.2 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From 8bc77f04210020c53d8ab398301f9da4e80a0c1c Mon Sep 17 00:00:00 2001 From: tetienne Date: Mon, 3 Feb 2020 17:27:01 +0100 Subject: [PATCH 086/378] Add color to light template (#31435) * Add color support to light template * Add tests about color support --- homeassistant/components/template/light.py | 70 +++++++++++- tests/components/template/test_light.py | 123 ++++++++++++++++++++- 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index c5512461f34..a6855a1654b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -6,8 +6,10 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light, ) @@ -42,6 +44,8 @@ CONF_LEVEL_ACTION = "set_level" CONF_LEVEL_TEMPLATE = "level_template" CONF_TEMPERATURE_TEMPLATE = "temperature_template" CONF_TEMPERATURE_ACTION = "set_temperature" +CONF_COLOR_TEMPLATE = "color_template" +CONF_COLOR_ACTION = "set_color" LIGHT_SCHEMA = vol.Schema( { @@ -57,6 +61,8 @@ LIGHT_SCHEMA = vol.Schema( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_COLOR_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA, } ) @@ -76,14 +82,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - level_template = device_config.get(CONF_LEVEL_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] + level_action = device_config.get(CONF_LEVEL_ACTION) + level_template = device_config.get(CONF_LEVEL_TEMPLATE) + temperature_action = device_config.get(CONF_TEMPERATURE_ACTION) temperature_template = device_config.get(CONF_TEMPERATURE_TEMPLATE) + color_action = device_config.get(CONF_COLOR_ACTION) + color_template = device_config.get(CONF_COLOR_TEMPLATE) + templates = { CONF_VALUE_TEMPLATE: state_template, CONF_ICON_TEMPLATE: icon_template, @@ -91,6 +102,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, CONF_LEVEL_TEMPLATE: level_template, CONF_TEMPERATURE_TEMPLATE: temperature_template, + CONF_COLOR_TEMPLATE: color_template, } initialise_templates(hass, templates) @@ -114,6 +126,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, temperature_action, temperature_template, + color_action, + color_template, ) ) @@ -144,6 +158,8 @@ class LightTemplate(Light): entity_ids, temperature_action, temperature_template, + color_action, + color_template, ): """Initialize the light.""" self.hass = hass @@ -165,12 +181,17 @@ class LightTemplate(Light): if temperature_action is not None: self._temperature_script = Script(hass, temperature_action) self._temperature_template = temperature_template + self._color_script = None + if color_action is not None: + self._color_script = Script(hass, color_action) + self._color_template = color_template self._state = False self._icon = None self._entity_picture = None self._brightness = None self._temperature = None + self._color = None self._entities = entity_ids self._available = True @@ -184,6 +205,11 @@ class LightTemplate(Light): """Return the CT color value in mireds.""" return self._temperature + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return self._color + @property def name(self): """Return the display name of this light.""" @@ -197,6 +223,8 @@ class LightTemplate(Light): supported_features |= SUPPORT_BRIGHTNESS if self._temperature_script is not None: supported_features |= SUPPORT_COLOR_TEMP + if self._color_script is not None: + supported_features |= SUPPORT_COLOR return supported_features @property @@ -239,6 +267,7 @@ class LightTemplate(Light): self._template is not None or self._level_template is not None or self._temperature_template is not None + or self._color_template is not None or self._availability_template is not None ): async_track_state_change( @@ -282,6 +311,12 @@ class LightTemplate(Light): await self._temperature_script.async_run( {"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context ) + elif ATTR_HS_COLOR in kwargs and self._color_script: + hs_value = kwargs[ATTR_HS_COLOR] + await self._color_script.async_run( + {"hs": hs_value, "h": int(hs_value[0]), "s": int(hs_value[1])}, + context=self._context, + ) else: await self._on_script.async_run() @@ -303,6 +338,8 @@ class LightTemplate(Light): self.update_temperature() + self.update_color() + for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), @@ -396,3 +433,34 @@ class LightTemplate(Light): except TemplateError: _LOGGER.error("Cannot evaluate temperature template", exc_info=True) self._temperature = None + + @callback + def update_color(self): + """Update the hs_color from the template.""" + if self._color_template is None: + return + + self._color = None + + try: + render = self._color_template.async_render() + h_str, s_str = map( + float, render.replace("(", "").replace(")", "").split(",", 1) + ) + if ( + h_str is not None + and s_str is not None + and 0 <= h_str <= 360 + and 0 <= s_str <= 100 + ): + self._color = (h_str, s_str) + elif h_str is not None and s_str is not None: + _LOGGER.error( + "Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)", + h_str, + s_str, + ) + else: + _LOGGER.error("Received invalid hs_color : (%s)", render) + except TemplateError as ex: + _LOGGER.error(ex) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 3e1ec207169..dccca97a1cc 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,7 +4,11 @@ import logging import pytest from homeassistant import setup -from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, +) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import callback @@ -816,6 +820,123 @@ class TestTemplateLight: assert state.attributes["entity_picture"] == "/local/light.png" + def test_color_action_no_template(self): + """Test setting color with optimistic template.""" + assert setup.setup_component( + self.hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{1 == 1}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_color": [ + { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "h": "{{h}}", + "s": "{{s}}", + }, + }, + { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "s": "{{s}}", + "h": "{{h}}", + }, + }, + ], + } + }, + } + }, + ) + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("light.test_template_light") + assert state.attributes.get("hs_color") is None + + common.turn_on( + self.hass, "light.test_template_light", **{ATTR_HS_COLOR: (40, 50)} + ) + self.hass.block_till_done() + assert len(self.calls) == 2 + assert self.calls[0].data["h"] == "40" + assert self.calls[0].data["s"] == "50" + assert self.calls[1].data["h"] == "40" + assert self.calls[1].data["s"] == "50" + + state = self.hass.states.get("light.test_template_light") + _LOGGER.info(str(state.attributes)) + assert state is not None + assert self.calls[0].data["h"] == "40" + assert self.calls[0].data["s"] == "50" + assert self.calls[1].data["h"] == "40" + assert self.calls[1].data["s"] == "50" + + @pytest.mark.parametrize( + "expected_hs,template", + [ + ((360, 100), "{{(360, 100)}}"), + ((359.9, 99.9), "{{(359.9, 99.9)}}"), + (None, "{{(361, 100)}}"), + (None, "{{(360, 101)}}"), + (None, "{{x - 12}}"), + ], + ) + def test_color_template(self, expected_hs, template): + """Test the template for the color.""" + with assert_setup_component(1, "light"): + assert setup.setup_component( + self.hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_color": [ + { + "service": "input_number.set_value", + "data_template": { + "entity_id": "input_number.h", + "color_temp": "{{h}}", + }, + } + ], + "color_template": template, + } + }, + } + }, + ) + self.hass.start() + self.hass.block_till_done() + state = self.hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("hs_color") == expected_hs + async def test_available_template_with_entities(hass): """Test availability templates with values from other entities.""" From ad5db476181fb53e39e5c6ba801b5ec97bcdddab Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 3 Feb 2020 20:21:38 +0100 Subject: [PATCH 087/378] Clean up Tahoma smartlock (#31430) * Added lock support for tahoma * Removed unused constant. --- homeassistant/components/tahoma/lock.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py index e320fd9e13d..0b02975fc7e 100644 --- a/homeassistant/components/tahoma/lock.py +++ b/homeassistant/components/tahoma/lock.py @@ -10,10 +10,13 @@ from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=120) +TAHOMA_STATE_LOCKED = "locked" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tahoma lock.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for device in hass.data[TAHOMA_DOMAIN]["devices"]["lock"]: @@ -37,13 +40,9 @@ class TahomaLock(TahomaDevice, LockDevice): self.controller.get_states([self.tahoma_device]) self._battery_level = self.tahoma_device.active_states["core:BatteryState"] self._name = self.tahoma_device.active_states["core:NameState"] - if self._battery_level == "low": - _LOGGER.warning("Lock %s has low battery", self._name) - if self._battery_level == "verylow": - _LOGGER.error("Lock %s has very low battery", self._name) if ( self.tahoma_device.active_states.get("core:LockedUnlockedState") - == STATE_LOCKED + == TAHOMA_STATE_LOCKED ): self._lock_status = STATE_LOCKED else: From 74fd57e23a107ecb53df6588ddef29eb5204c20c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 3 Feb 2020 12:22:50 -0700 Subject: [PATCH 088/378] Fix issue with Notion dispatcher topic (#31441) * Fix issue with Notion dispatcher topic * Use f-string --- homeassistant/components/notion/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index 2f81cb72ac0..6ce5c4e5bc7 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -7,7 +7,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) DATA_CLIENT = "client" -TOPIC_DATA_UPDATE = "data_update" +TOPIC_DATA_UPDATE = f"{DOMAIN}_data_update" TYPE_BINARY_SENSOR = "binary_sensor" TYPE_SENSOR = "sensor" From e799b082157615efffbd63fd14479107712e77a3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 3 Feb 2020 12:23:06 -0700 Subject: [PATCH 089/378] Fix issue with IQVIA dispatcher topic (#31440) * Fix issues with IQVIA dispatcher topic * Use f-string --- homeassistant/components/iqvia/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index 09548ee929a..52e657bc2c0 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -6,7 +6,7 @@ CONF_ZIP_CODE = "zip_code" DATA_CLIENT = "client" DATA_LISTENER = "listener" -TOPIC_DATA_UPDATE = "data_update" +TOPIC_DATA_UPDATE = f"{DOMAIN}_data_update" TYPE_ALLERGY_FORECAST = "allergy_average_forecasted" TYPE_ALLERGY_INDEX = "allergy_index" From 30e803d70bccbd8bb8d79e9c7e330771e393421d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 3 Feb 2020 12:23:19 -0700 Subject: [PATCH 090/378] Fix issue with WWLLN dispatcher topic (#31442) --- homeassistant/components/wwlln/geo_location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wwlln/geo_location.py b/homeassistant/components/wwlln/geo_location.py index e8dd7ec08c7..e1ca47664d5 100644 --- a/homeassistant/components/wwlln/geo_location.py +++ b/homeassistant/components/wwlln/geo_location.py @@ -35,7 +35,7 @@ DEFAULT_EVENT_NAME = "Lightning Strike: {0}" DEFAULT_ICON = "mdi:flash" DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) -SIGNAL_DELETE_ENTITY = "delete_entity_{0}" +SIGNAL_DELETE_ENTITY = "wwlln_delete_entity_{0}" async def async_setup_entry(hass, entry, async_add_entities): From bea7aae8cd87aaef58359383d8c0ac0c34ef6abd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 3 Feb 2020 12:23:51 -0700 Subject: [PATCH 091/378] Fix issues with Ambient PWS dispatcher topic (#31439) * Correct over-broad Ambient PWS data updates * Make sure we provide a callable * Don't use a partial --- homeassistant/components/ambient_station/__init__.py | 4 ++-- homeassistant/components/ambient_station/const.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index c61e15dfeb5..0bbb7a760fe 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -378,7 +378,7 @@ class AmbientStation: if data != self.stations[mac_address][ATTR_LAST_DATA]: _LOGGER.debug("New data received: %s", data) self.stations[mac_address][ATTR_LAST_DATA] = data - async_dispatcher_send(self._hass, TOPIC_UPDATE) + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(mac_address)) _LOGGER.debug("Resetting watchdog") self._watchdog_listener() @@ -518,7 +518,7 @@ class AmbientWeatherEntity(Entity): self.async_schedule_update_ha_state(True) self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.hass, TOPIC_UPDATE.format(self._mac_address), update ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index 21a6e514b30..4f94e1cfe88 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,7 +8,7 @@ CONF_APP_KEY = "app_key" DATA_CLIENT = "data_client" -TOPIC_UPDATE = "update" +TOPIC_UPDATE = "ambient_station_data_update_{0}" TYPE_BINARY_SENSOR = "binary_sensor" TYPE_SENSOR = "sensor" From 03642d902902a981caffea4f461f3d2f789df369 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 3 Feb 2020 20:27:20 +0100 Subject: [PATCH 092/378] Add missing await to HMIPC (#31415) * Add missing await to HMIPC * use callback instead * Fix call, move callback to hap --- .../components/homematicip_cloud/__init__.py | 7 +------ homeassistant/components/homematicip_cloud/hap.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 63d1f82071b..efc7245de8b 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -348,14 +348,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if not await hap.async_setup(): return False - async def async_reset_hap_connection(): - """Reset hmip hap connection.""" - await hap.async_reset() - _LOGGER.debug("Reset connection to access point id %s", entry.unique_id) - # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection hap.reset_connection_listener = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_reset_hap_connection() + EVENT_HOMEASSISTANT_STOP, hap.shutdown ) # Register hap as device in registry. diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 8a4b7ed5fae..d64d05e72aa 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -224,6 +224,17 @@ class HomematicipHAP: self.hmip_device_by_entity_id = {} return True + @callback + def shutdown(self, event) -> None: + """Wrap the call to async_reset. + + Used as an argument to EventBus.async_listen_once. + """ + self.hass.async_create_task(self.async_reset()) + _LOGGER.debug( + "Reset connection to access point id %s", self.config_entry.unique_id + ) + async def get_hap( self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str ) -> AsyncHome: From 4550968316967cfa111813b263b715afdbd8e553 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 3 Feb 2020 20:34:02 +0100 Subject: [PATCH 093/378] Samsung TV refinements (#31248) * use st not deviceType * show model in flow title * Update strings.json * add re-auth to entity * add re-auth to config_flow * handle auth popup better * use media player domain const * fix tests * rename not_found to not_successful * authz not authn * Update media_player.py * Update config_flow.py * Update media_player.py * Update test_media_player.py * finalize re-auth * fix ssd tests * better naming * fix ip-address-mock serialization * fix turn_on_action serialization * add type of hass object * fix acces denied test * remove half-added typing * async get ip address * fix pylint --- .../components/samsungtv/__init__.py | 10 ++- .../components/samsungtv/config_flow.py | 70 +++++++++++-------- homeassistant/components/samsungtv/const.py | 2 +- .../components/samsungtv/manifest.json | 2 +- .../components/samsungtv/media_player.py | 35 ++++++++-- .../components/samsungtv/strings.json | 11 +-- homeassistant/generated/ssdp.py | 2 +- .../components/samsungtv/test_config_flow.py | 28 ++++---- tests/components/samsungtv/test_init.py | 8 ++- .../components/samsungtv/test_media_player.py | 41 ++++++++--- 10 files changed, 138 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 5647b407bfb..bc49dc3156d 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -3,6 +3,7 @@ import socket import voluptuous as vol +from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -41,7 +42,14 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Samsung TV integration.""" if DOMAIN in config: + hass.data[DOMAIN] = {} for entry_config in config[DOMAIN]: + ip_address = await hass.async_add_executor_job( + socket.gethostbyname, entry_config[CONF_HOST] + ) + hass.data[DOMAIN][ip_address] = { + CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION) + } hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=entry_config @@ -54,7 +62,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up the Samsung TV platform.""" hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") + hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) ) return True diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 0bf39cc248b..debe7349b6c 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME, ATTR_UPNP_UDN, @@ -24,20 +23,13 @@ from homeassistant.const import ( ) # pylint:disable=unused-import -from .const import ( - CONF_MANUFACTURER, - CONF_MODEL, - CONF_ON_ACTION, - DOMAIN, - LOGGER, - METHODS, -) +from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER, METHODS DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) RESULT_AUTH_MISSING = "auth_missing" RESULT_SUCCESS = "success" -RESULT_NOT_FOUND = "not_found" +RESULT_NOT_SUCCESSFUL = "not_successful" RESULT_NOT_SUPPORTED = "not_supported" @@ -63,23 +55,21 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._method = None self._model = None self._name = None - self._on_script = None self._port = None self._title = None - self._uuid = None + self._id = None def _get_entry(self): return self.async_create_entry( title=self._title, data={ CONF_HOST: self._host, - CONF_ID: self._uuid, + CONF_ID: self._id, CONF_IP_ADDRESS: self._ip, CONF_MANUFACTURER: self._manufacturer, CONF_METHOD: self._method, CONF_MODEL: self._model, CONF_NAME: self._name, - CONF_ON_ACTION: self._on_script, CONF_PORT: self._port, }, ) @@ -94,7 +84,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "host": self._host, "method": method, "port": self._port, - "timeout": 1, + # We need this high timeout because waiting for auth popup is just an open socket + "timeout": 31, } try: LOGGER.debug("Try config: %s", config) @@ -108,15 +99,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except UnhandledResponse: LOGGER.debug("Working but unsupported config: %s", config) return RESULT_NOT_SUPPORTED - except (OSError): - LOGGER.debug("Failing config: %s", config) + except OSError as err: + LOGGER.debug("Failing config: %s, error: %s", config, err) LOGGER.debug("No working config found") - return RESULT_NOT_FOUND + return RESULT_NOT_SUCCESSFUL async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - self._on_script = user_input.get(CONF_ON_ACTION) self._port = user_input.get(CONF_PORT) return await self.async_step_user(user_input) @@ -133,7 +123,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = user_input.get(CONF_HOST) self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._title = user_input.get(CONF_NAME) + self._name = user_input.get(CONF_NAME) + self._title = self._name result = await self.hass.async_add_executor_job(self._try_connect) @@ -150,24 +141,27 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = host self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._manufacturer = user_input[ATTR_UPNP_MANUFACTURER] - self._model = user_input[ATTR_UPNP_MODEL_NAME] - self._name = user_input[ATTR_UPNP_FRIENDLY_NAME] - if self._name.startswith("[TV]"): - self._name = self._name[4:] - self._title = f"{self._name} ({self._model})" - self._uuid = user_input[ATTR_UPNP_UDN] - if self._uuid.startswith("uuid:"): - self._uuid = self._uuid[5:] + self._manufacturer = user_input.get(ATTR_UPNP_MANUFACTURER) + self._model = user_input.get(ATTR_UPNP_MODEL_NAME) + self._name = f"Samsung {self._model}" + self._id = user_input.get(ATTR_UPNP_UDN) + self._title = self._model + + # probably access denied + if self._id is None: + return self.async_abort(reason=RESULT_AUTH_MISSING) + if self._id.startswith("uuid:"): + self._id = self._id[5:] config_entry = await self.async_set_unique_id(ip_address) if config_entry: - config_entry.data[CONF_ID] = self._uuid + config_entry.data[CONF_ID] = self._id config_entry.data[CONF_MANUFACTURER] = self._manufacturer config_entry.data[CONF_MODEL] = self._model self.hass.config_entries.async_update_entry(config_entry) return self.async_abort(reason="already_configured") + self.context["title_placeholders"] = {"model": self._model} return await self.async_step_confirm() async def async_step_confirm(self, user_input=None): @@ -182,3 +176,19 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="confirm", description_placeholders={"model": self._model} ) + + async def async_step_reauth(self, user_input=None): + """Handle configuration by re-auth.""" + self._host = user_input[CONF_HOST] + self._id = user_input.get(CONF_ID) + self._ip = user_input[CONF_IP_ADDRESS] + self._manufacturer = user_input.get(CONF_MANUFACTURER) + self._model = user_input.get(CONF_MODEL) + self._name = user_input.get(CONF_NAME) + self._port = user_input.get(CONF_PORT) + self._title = self._model or self._name + + await self.async_set_unique_id(self._ip) + self.context["title_placeholders"] = {"model": self._title} + + return await self.async_step_confirm() diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index 7cf71e406cb..ea893390a5b 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -4,7 +4,7 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "samsungtv" -DEFAULT_NAME = "Samsung TV Remote" +DEFAULT_NAME = "Samsung TV" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 0d0a360fc20..3adc3b52eb3 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ ], "ssdp": [ { - "deviceType": "urn:samsung.com:device:RemoteControlReceiver:1" + "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], "dependencies": [], diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index aca54838a99..8de42d157b7 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -23,7 +23,9 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( CONF_HOST, CONF_ID, + CONF_IP_ADDRESS, CONF_METHOD, + CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON, @@ -59,8 +61,16 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Samsung TV from a config entry.""" - turn_on_action = config_entry.data.get(CONF_ON_ACTION) - on_script = Script(hass, turn_on_action) if turn_on_action else None + ip_address = config_entry.data[CONF_IP_ADDRESS] + on_script = None + if ( + DOMAIN in hass.data + and ip_address in hass.data[DOMAIN] + and CONF_ON_ACTION in hass.data[DOMAIN][ip_address] + and hass.data[DOMAIN][ip_address][CONF_ON_ACTION] + ): + turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] + on_script = Script(hass, turn_on_action) async_add_entities([SamsungTVDevice(config_entry, on_script)]) @@ -70,12 +80,11 @@ class SamsungTVDevice(MediaPlayerDevice): def __init__(self, config_entry, on_script): """Initialize the Samsung device.""" self._config_entry = config_entry - self._name = config_entry.title - self._uuid = config_entry.data.get(CONF_ID) self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) self._model = config_entry.data.get(CONF_MODEL) + self._name = config_entry.data.get(CONF_NAME) self._on_script = on_script - self._update_listener = None + self._uuid = config_entry.data.get(CONF_ID) # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -88,7 +97,7 @@ class SamsungTVDevice(MediaPlayerDevice): # Generate a configuration for the Samsung library self._config = { "name": "HomeAssistant", - "description": self._name, + "description": "HomeAssistant", "id": "ha.component.samsung", "method": config_entry.data[CONF_METHOD], "port": config_entry.data.get(CONF_PORT), @@ -124,7 +133,19 @@ class SamsungTVDevice(MediaPlayerDevice): """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. - self._remote = SamsungRemote(self._config.copy()) + try: + self._remote = SamsungRemote(self._config.copy()) + # This is only happening when the auth was switched to DENY + # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket + except samsung_exceptions.AccessDenied: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=self._config_entry.data, + ) + ) + raise return self._remote diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index ee762503e5c..2e36062669f 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -1,10 +1,11 @@ { "config": { + "flow_title": "Samsung TV: {model}", "title": "Samsung TV", "step": { "user": { "title": "Samsung TV", - "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authentication.", + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", "data": { "host": "Host or IP address", "name": "Name" @@ -12,15 +13,15 @@ }, "confirm": { "title": "Samsung TV", - "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authentication. Manual configurations for this TV will be overwritten." + "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten." } }, "abort": { "already_in_progress": "Samsung TV configuration is already in progress.", "already_configured": "This Samsung TV is already configured.", - "auth_missing": "Home Assistant is not authenticated to connect to this Samsung TV.", - "not_found": "No supported Samsung TV devices found on the network.", - "not_supported": "This Samsung TV devices is currently not supported." + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", + "not_successful": "Unable to connect to this Samsung TV device.", + "not_supported": "This Samsung TV device is currently not supported." } } } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 83f375f031b..bea04484b11 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -38,7 +38,7 @@ SSDP = { ], "samsungtv": [ { - "deviceType": "urn:samsung.com:device:RemoteControlReceiver:1" + "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], "sonos": [ diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index ce6741f0703..9c8ec3a9a09 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -42,7 +42,7 @@ AUTODETECT_WEBSOCKET = { "method": "websocket", "port": None, "host": "fake_host", - "timeout": 1, + "timeout": 31, } AUTODETECT_LEGACY = { "name": "HomeAssistant", @@ -51,7 +51,7 @@ AUTODETECT_LEGACY = { "method": "legacy", "port": None, "host": "fake_host", - "timeout": 1, + "timeout": 31, } @@ -87,7 +87,7 @@ async def test_user(hass, remote): assert result["type"] == "create_entry" assert result["title"] == "fake_name" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] is None + assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_MANUFACTURER] is None assert result["data"][CONF_MODEL] is None assert result["data"][CONF_ID] is None @@ -123,19 +123,19 @@ async def test_user_not_supported(hass): assert result["reason"] == "not_supported" -async def test_user_not_found(hass): - """Test starting a flow by user but no device found.""" +async def test_user_not_successful(hass): + """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.config_flow.Remote", side_effect=OSError("Boom"), ), patch("homeassistant.components.samsungtv.config_flow.socket"): - # device not found + # device not connectable result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_found" + assert result["reason"] == "not_successful" async def test_user_already_configured(hass, remote): @@ -170,9 +170,9 @@ async def test_ssdp(hass, remote): result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "fake_name (fake_model)" + assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_name" + assert result["data"][CONF_NAME] == "Samsung fake_model" assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" assert result["data"][CONF_ID] == "fake_uuid" @@ -193,9 +193,9 @@ async def test_ssdp_noprefix(hass, remote): result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "fake2_name (fake2_model)" + assert result["title"] == "fake2_model" assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "fake2_name" + assert result["data"][CONF_NAME] == "Samsung fake2_model" assert result["data"][CONF_MANUFACTURER] == "fake2_manufacturer" assert result["data"][CONF_MODEL] == "fake2_model" assert result["data"][CONF_ID] == "fake2_uuid" @@ -245,7 +245,7 @@ async def test_ssdp_not_supported(hass): assert result["reason"] == "not_supported" -async def test_ssdp_not_found(hass): +async def test_ssdp_not_successful(hass): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.config_flow.Remote", @@ -264,7 +264,7 @@ async def test_ssdp_not_found(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "not_found" + assert result["reason"] == "not_successful" async def test_ssdp_already_in_progress(hass, remote): @@ -380,7 +380,7 @@ async def test_autodetect_none(hass, remote): DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_found" + assert result["reason"] == "not_successful" assert remote.call_count == 2 assert remote.call_args_list == [ call(AUTODETECT_WEBSOCKET), diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 55ec52b56ae..cd31434e6b0 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -32,7 +32,7 @@ MOCK_CONFIG = { } REMOTE_CALL = { "name": "HomeAssistant", - "description": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_NAME], + "description": "HomeAssistant", "id": "ha.component.samsung", "method": "websocket", "port": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_PORT], @@ -44,11 +44,13 @@ REMOTE_CALL = { @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch("homeassistant.components.samsungtv.socket"), patch( + with patch("homeassistant.components.samsungtv.socket") as socket1, patch( "homeassistant.components.samsungtv.config_flow.socket" - ), patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( + ) as socket2, patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( "homeassistant.components.samsungtv.media_player.SamsungRemote" ) as remote: + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remote diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2b9f379515d..ba245ce7d6f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -75,15 +75,17 @@ MOCK_CONFIG_NOTURNON = { @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch("homeassistant.components.samsungtv.config_flow.socket"), patch( - "homeassistant.components.samsungtv.config_flow.Remote" - ), patch( + with patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( + "homeassistant.components.samsungtv.config_flow.socket" + ) as socket1, patch( "homeassistant.components.samsungtv.media_player.SamsungRemote" ) as remote_class, patch( "homeassistant.components.samsungtv.socket" - ): + ) as socket2: remote = mock.Mock() remote_class.return_value = remote + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remote @@ -135,11 +137,12 @@ async def test_update_on(hass, remote, mock_now): async def test_update_off(hass, remote, mock_now): """Testing update tv off.""" + await setup_samsungtv(hass, MOCK_CONFIG) + with patch( "homeassistant.components.samsungtv.media_player.SamsungRemote", side_effect=[OSError("Boom"), mock.DEFAULT], - ), patch("homeassistant.components.samsungtv.config_flow.socket"): - await setup_samsungtv(hass, MOCK_CONFIG) + ): next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): @@ -150,13 +153,35 @@ async def test_update_off(hass, remote, mock_now): assert state.state == STATE_OFF +async def test_update_access_denied(hass, remote, mock_now): + """Testing update tv unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=exceptions.AccessDenied("Boom"), + ): + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"]["source"] == "reauth" + ] + + async def test_update_unhandled_response(hass, remote, mock_now): """Testing update tv unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + with patch( "homeassistant.components.samsungtv.media_player.SamsungRemote", side_effect=[exceptions.UnhandledResponse("Boom"), mock.DEFAULT], - ), patch("homeassistant.components.samsungtv.config_flow.socket"): - await setup_samsungtv(hass, MOCK_CONFIG) + ): next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): From 114d48ed8b5d029abe2fb06cba0eb4aebda7eca4 Mon Sep 17 00:00:00 2001 From: Konsts Date: Mon, 3 Feb 2020 22:23:58 +0200 Subject: [PATCH 094/378] Add timeout attribute for send files (#31379) * Add timeout attribute for send files * Remove dublicate Remove attribute dublicate in BASE_SERVICE_SCHEMA * Remove duplicate return attribute to BASE_SERVICE_SCHEMA and remove from SERVICE_SCHEMA_SEND_FILE * Add timeout parameter description Add timeout parameter description for "send message" and "send location" --- .../components/telegram_bot/__init__.py | 10 +++++----- .../components/telegram_bot/services.yaml | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 9b56201f8c7..277f9108663 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONF_API_KEY, CONF_PLATFORM, - CONF_TIMEOUT, CONF_URL, HTTP_DIGEST_AUTHENTICATION, ) @@ -67,6 +66,7 @@ ATTR_URL = "url" ATTR_USER_ID = "user_id" ATTR_USERNAME = "username" ATTR_VERIFY_SSL = "verify_ssl" +ATTR_TIMEOUT = "timeout" CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids" CONF_PROXY_URL = "proxy_url" @@ -135,7 +135,7 @@ BASE_SERVICE_SCHEMA = vol.Schema( vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean, vol.Optional(ATTR_KEYBOARD): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list, - vol.Optional(CONF_TIMEOUT): vol.Coerce(float), + vol.Optional(ATTR_TIMEOUT): cv.positive_int, }, extra=vol.ALLOW_EXTRA, ) @@ -499,15 +499,15 @@ class TelegramNotificationService: ATTR_DISABLE_WEB_PREV: None, ATTR_REPLY_TO_MSGID: None, ATTR_REPLYMARKUP: None, - CONF_TIMEOUT: None, + ATTR_TIMEOUT: None, } if data is not None: if ATTR_PARSER in data: params[ATTR_PARSER] = self._parsers.get( data[ATTR_PARSER], self._parse_mode ) - if CONF_TIMEOUT in data: - params[CONF_TIMEOUT] = data[CONF_TIMEOUT] + if ATTR_TIMEOUT in data: + params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT] if ATTR_DISABLE_NOTIF in data: params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF] if ATTR_DISABLE_WEB_PREV in data: diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index ed8720c5877..e3d303a2c52 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -21,6 +21,9 @@ send_message: disable_web_page_preview: description: Disables link previews for links in the message. example: true + timeout: + description: Timeout for send message. 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. Empty list clears a previously set keyboard. example: '["/command1, /command2", "/command3"]' @@ -55,6 +58,9 @@ send_photo: 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 photo. 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"]' @@ -86,6 +92,9 @@ send_sticker: 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 sticker. 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"]' @@ -120,6 +129,9 @@ send_video: 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 video. 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"]' @@ -154,6 +166,9 @@ send_document: 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 document. 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"]' @@ -176,6 +191,9 @@ send_location: 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 + timeout: + description: Timeout for send photo. 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"]' From f5b790054a32972fd4067ea9458f81b72d3b63b3 Mon Sep 17 00:00:00 2001 From: Kasper Kirkegaard Date: Mon, 3 Feb 2020 21:31:53 +0100 Subject: [PATCH 095/378] Fix misspelled sensor names (#31344) --- homeassistant/components/danfoss_air/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index ea0002d0ac3..247eb955154 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -48,10 +48,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ["Danfoss Air Remaining Filter", "%", ReadCommand.filterPercent, None], ["Danfoss Air Humidity", "%", ReadCommand.humidity, DEVICE_CLASS_HUMIDITY], ["Danfoss Air Fan Step", "%", ReadCommand.fan_step, None], - ["Dandoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None], - ["Dandoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None], + ["Danfoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None], + ["Danfoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None], [ - "Dandoss Air Dial Battery", + "Danfoss Air Dial Battery", "%", ReadCommand.battery_percent, DEVICE_CLASS_BATTERY, From 3f9dbe684519f81b6f08b0c51859224b9d98450d Mon Sep 17 00:00:00 2001 From: quthla Date: Mon, 3 Feb 2020 23:09:25 +0100 Subject: [PATCH 096/378] Fix theme color (#31366) --- homeassistant/components/frontend/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8039b9947e7..fdea21fe91e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -342,10 +342,12 @@ def _async_setup_themes(hass, themes): """Update theme_color in manifest.""" name = hass.data[DATA_DEFAULT_THEME] themes = hass.data[DATA_THEMES] - if name != DEFAULT_THEME and PRIMARY_COLOR in themes[name]: - MANIFEST_JSON["theme_color"] = themes[name][PRIMARY_COLOR] - else: - MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR + MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR + if name != DEFAULT_THEME: + MANIFEST_JSON["theme_color"] = themes[name].get( + "app-header-background-color", + themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), + ) hass.bus.async_fire( EVENT_THEMES_UPDATED, {"themes": themes, "default_theme": name} ) From c8d9b83b24107b35b68abe1ab8c61f005c9b239b Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 3 Feb 2020 17:20:39 -0500 Subject: [PATCH 097/378] Update StepSpeaker and Speaker interfaces in Alexa (#31444) * Yield only one Speaker interface. * Yield PowerController only is supported. * Revert "Yield PowerController only is supported." This reverts commit c0dbf7e4 * Add Alexa.Speaker interface properties. * Refactor tests for Alexa.Speaker and Alexa.StepSpeaker. * Code Smell Change. * Fix R1705: Unnecessary "elif" after "return". --- .../components/alexa/capabilities.py | 38 +++ homeassistant/components/alexa/entities.py | 7 +- tests/components/alexa/test_smart_home.py | 221 ++++++++++-------- 3 files changed, 165 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index eb1474aed7e..eba59e92cd4 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,5 +1,6 @@ """Alexa capabilities.""" import logging +import math from homeassistant.components import ( cover, @@ -645,6 +646,43 @@ class AlexaSpeaker(AlexaCapability): """Return the Alexa API name of this interface.""" return "Alexa.Speaker" + def properties_supported(self): + """Return what properties this entity supports.""" + properties = [{"name": "volume"}] + + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & media_player.SUPPORT_VOLUME_MUTE: + properties.append({"name": "muted"}) + + return properties + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name == "volume": + current_level = self.entity.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL + ) + try: + current = math.floor(int(current_level * 100)) + except ZeroDivisionError: + current = 0 + return current + + if name == "muted": + return bool( + self.entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) + ) + + return None + class AlexaStepSpeaker(AlexaCapability): """Implements Alexa.StepSpeaker. diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 254cec44553..8801936b35b 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -508,12 +508,7 @@ class MediaPlayerCapabilities(AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.const.SUPPORT_VOLUME_SET: yield AlexaSpeaker(self.entity) - - step_volume_features = ( - media_player.const.SUPPORT_VOLUME_MUTE - | media_player.const.SUPPORT_VOLUME_STEP - ) - if supported & step_volume_features: + elif supported & media_player.const.SUPPORT_VOLUME_STEP: yield AlexaStepSpeaker(self.entity) playback_features = ( diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ca6b1e1ccb6..3e8e87f62c9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -16,6 +16,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, ) import homeassistant.components.vacuum as vacuum from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -904,7 +905,6 @@ async def test_media_player(hass): "Alexa.PlaybackStateReporter", "Alexa.PowerController", "Alexa.Speaker", - "Alexa.StepSpeaker", ) playback_capability = get_capability(capabilities, "Alexa.PlaybackController") @@ -958,93 +958,6 @@ async def test_media_player(hass): hass, ) - call, _ = await assert_request_calls_service( - "Alexa.Speaker", - "SetVolume", - "media_player#test", - "media_player.volume_set", - hass, - payload={"volume": 50}, - ) - assert call.data["volume_level"] == 0.5 - - call, _ = await assert_request_calls_service( - "Alexa.Speaker", - "SetMute", - "media_player#test", - "media_player.volume_mute", - hass, - payload={"mute": True}, - ) - assert call.data["is_volume_muted"] - - call, _, = await assert_request_calls_service( - "Alexa.Speaker", - "SetMute", - "media_player#test", - "media_player.volume_mute", - hass, - payload={"mute": False}, - ) - assert not call.data["is_volume_muted"] - - await assert_percentage_changes( - hass, - [(0.7, "-5"), (0.8, "5"), (0, "-80")], - "Alexa.Speaker", - "AdjustVolume", - "media_player#test", - "volume", - "media_player.volume_set", - "volume_level", - ) - - call, _ = await assert_request_calls_service( - "Alexa.StepSpeaker", - "SetMute", - "media_player#test", - "media_player.volume_mute", - hass, - payload={"mute": True}, - ) - assert call.data["is_volume_muted"] - - call, _, = await assert_request_calls_service( - "Alexa.StepSpeaker", - "SetMute", - "media_player#test", - "media_player.volume_mute", - hass, - payload={"mute": False}, - ) - assert not call.data["is_volume_muted"] - - call, _ = await assert_request_calls_service( - "Alexa.StepSpeaker", - "AdjustVolume", - "media_player#test", - "media_player.volume_up", - hass, - payload={"volumeSteps": 1, "volumeStepsDefault": False}, - ) - - call, _ = await assert_request_calls_service( - "Alexa.StepSpeaker", - "AdjustVolume", - "media_player#test", - "media_player.volume_down", - hass, - payload={"volumeSteps": -1, "volumeStepsDefault": False}, - ) - - call, _ = await assert_request_calls_service( - "Alexa.StepSpeaker", - "AdjustVolume", - "media_player#test", - "media_player.volume_up", - hass, - payload={"volumeSteps": 10, "volumeStepsDefault": True}, - ) call, _ = await assert_request_calls_service( "Alexa.ChannelController", "ChangeChannel", @@ -1140,7 +1053,6 @@ async def test_media_player_power(hass): "Alexa.PowerController", "Alexa.SeekController", "Alexa.Speaker", - "Alexa.StepSpeaker", ) await assert_request_calls_service( @@ -1265,22 +1177,141 @@ async def test_media_player_inputs(hass): async def test_media_player_speaker(hass): - """Test media player discovery with device class speaker.""" + """Test media player with speaker interface.""" device = ( - "media_player.test", + "media_player.test_speaker", "off", { - "friendly_name": "Test media player", - "supported_features": 51765, + "friendly_name": "Test media player speaker", + "supported_features": SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, "volume_level": 0.75, "device_class": "speaker", }, ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "media_player#test" + assert appliance["endpointId"] == "media_player#test_speaker" assert appliance["displayCategories"][0] == "SPEAKER" - assert appliance["friendlyName"] == "Test media player" + assert appliance["friendlyName"] == "Test media player speaker" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.EndpointHealth", + "Alexa.PowerController", + "Alexa.Speaker", + ) + + speaker_capability = get_capability(capabilities, "Alexa.Speaker") + properties = speaker_capability["properties"] + assert {"name": "volume"} in properties["supported"] + assert {"name": "muted"} in properties["supported"] + + call, _ = await assert_request_calls_service( + "Alexa.Speaker", + "SetVolume", + "media_player#test_speaker", + "media_player.volume_set", + hass, + payload={"volume": 50}, + ) + assert call.data["volume_level"] == 0.5 + + call, _ = await assert_request_calls_service( + "Alexa.Speaker", + "SetMute", + "media_player#test_speaker", + "media_player.volume_mute", + hass, + payload={"mute": True}, + ) + assert call.data["is_volume_muted"] + + call, _, = await assert_request_calls_service( + "Alexa.Speaker", + "SetMute", + "media_player#test_speaker", + "media_player.volume_mute", + hass, + payload={"mute": False}, + ) + assert not call.data["is_volume_muted"] + + await assert_percentage_changes( + hass, + [(0.7, "-5"), (0.8, "5"), (0, "-80")], + "Alexa.Speaker", + "AdjustVolume", + "media_player#test_speaker", + "volume", + "media_player.volume_set", + "volume_level", + ) + + +async def test_media_player_step_speaker(hass): + """Test media player with step speaker interface.""" + device = ( + "media_player.test_step_speaker", + "off", + { + "friendly_name": "Test media player step speaker", + "supported_features": SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP, + "device_class": "speaker", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test_step_speaker" + assert appliance["displayCategories"][0] == "SPEAKER" + assert appliance["friendlyName"] == "Test media player step speaker" + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "SetMute", + "media_player#test_step_speaker", + "media_player.volume_mute", + hass, + payload={"mute": True}, + ) + assert call.data["is_volume_muted"] + + call, _, = await assert_request_calls_service( + "Alexa.StepSpeaker", + "SetMute", + "media_player#test_step_speaker", + "media_player.volume_mute", + hass, + payload={"mute": False}, + ) + assert not call.data["is_volume_muted"] + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test_step_speaker", + "media_player.volume_up", + hass, + payload={"volumeSteps": 1, "volumeStepsDefault": False}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test_step_speaker", + "media_player.volume_down", + hass, + payload={"volumeSteps": -1, "volumeStepsDefault": False}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test_step_speaker", + "media_player.volume_up", + hass, + payload={"volumeSteps": 10, "volumeStepsDefault": True}, + ) async def test_media_player_seek(hass): From 119566f280598dbaf6d6d9ecd2d342c870e97036 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Feb 2020 23:22:47 +0100 Subject: [PATCH 098/378] Keep track of the derivative for unit_time (#31397) * keep track of the derivative for unit_time In this way, you will get a better estimate of the derivate during the timescale that is relavant to the sensor. This solved a problem where sensors have a low output resolution. For example a temperature sensor that can only be integer numbers. It might report many values that are the same and then suddenly go up one value. Only in that moment (with the current implementation) the derivative will be finite. With my proposed implementation, this problem will not occur, because it takes the average derivative of the last `unit_time`. * only loop as much as needed * treat the special case of 1 entry * add option time_window * use cv.time_period * fix comment * set time_window=0 by default * rephrase comment * use timedelta for time_window * fix the "G" unit_prefix and add more prefixes https://en.wikipedia.org/wiki/Unit_prefix * add debugging lines * simplify logic * fix bug where the there was a division of unit_time instead of multiplication * simplify tests * add test_data_moving_average_for_discrete_sensor * fix test_dataSet6 * improve readability of the tests * better explain the test * remove debugging log lines --- homeassistant/components/derivative/sensor.py | 60 +++-- tests/components/derivative/test_sensor.py | 205 ++++++------------ 2 files changed, 113 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 177d1258f3c..5e68b268685 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -27,9 +27,19 @@ CONF_ROUND_DIGITS = "round" CONF_UNIT_PREFIX = "unit_prefix" CONF_UNIT_TIME = "unit_time" CONF_UNIT = "unit" +CONF_TIME_WINDOW = "time_window" # SI Metric prefixes -UNIT_PREFIXES = {None: 1, "k": 10 ** 3, "G": 10 ** 6, "T": 10 ** 9} +UNIT_PREFIXES = { + None: 1, + "n": 1e-9, + "µ": 1e-6, + "m": 1e-3, + "k": 1e3, + "M": 1e6, + "G": 1e9, + "T": 1e12, +} # SI Time prefixes UNIT_TIME = {"s": 1, "min": 60, "h": 60 * 60, "d": 24 * 60 * 60} @@ -37,6 +47,7 @@ UNIT_TIME = {"s": 1, "min": 60, "h": 60 * 60, "d": 24 * 60 * 60} ICON = "mdi:chart-line" DEFAULT_ROUND = 3 +DEFAULT_TIME_WINDOW = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -46,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default="h"): vol.In(UNIT_TIME), vol.Optional(CONF_UNIT): cv.string, + vol.Optional(CONF_TIME_WINDOW, default=DEFAULT_TIME_WINDOW): cv.time_period, } ) @@ -53,12 +65,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the derivative sensor.""" derivative = DerivativeSensor( - config[CONF_SOURCE], - config.get(CONF_NAME), - config[CONF_ROUND_DIGITS], - config[CONF_UNIT_PREFIX], - config[CONF_UNIT_TIME], - config.get(CONF_UNIT), + source_entity=config[CONF_SOURCE], + name=config.get(CONF_NAME), + round_digits=config[CONF_ROUND_DIGITS], + unit_prefix=config[CONF_UNIT_PREFIX], + unit_time=config[CONF_UNIT_TIME], + unit_of_measurement=config.get(CONF_UNIT), + time_window=config[CONF_TIME_WINDOW], ) async_add_entities([derivative]) @@ -75,11 +88,13 @@ class DerivativeSensor(RestoreEntity): unit_prefix, unit_time, unit_of_measurement, + time_window, ): """Initialize the derivative sensor.""" self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 + self._state_list = [] # List of tuples with (timestamp, sensor_value) self._name = name if name is not None else f"{source_entity} derivative" @@ -93,6 +108,7 @@ class DerivativeSensor(RestoreEntity): self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] + self._time_window = time_window.total_seconds() async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -114,6 +130,19 @@ class DerivativeSensor(RestoreEntity): ): return + now = new_state.last_updated + # Filter out the tuples that are older than (and outside of the) `time_window` + self._state_list = [ + (timestamp, state) + for timestamp, state in self._state_list + if (now - timestamp).total_seconds() < self._time_window + ] + # It can happen that the list is now empty, in that case + # we use the old_state, because we cannot do anything better. + if len(self._state_list) == 0: + self._state_list.append((old_state.last_updated, old_state.state)) + self._state_list.append((new_state.last_updated, new_state.state)) + if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._unit_of_measurement = self._unit_template.format( @@ -122,13 +151,16 @@ class DerivativeSensor(RestoreEntity): try: # derivative of previous measures. - gradient = 0 - elapsed_time = ( - new_state.last_updated - old_state.last_updated - ).total_seconds() - gradient = Decimal(new_state.state) - Decimal(old_state.state) - derivative = gradient / ( - Decimal(elapsed_time) * (self._unit_prefix * self._unit_time) + last_time, last_value = self._state_list[-1] + first_time, first_value = self._state_list[0] + + elapsed_time = (last_time - first_time).total_seconds() + delta_value = Decimal(last_value) - Decimal(first_value) + derivative = ( + delta_value + / Decimal(elapsed_time) + / Decimal(self._unit_prefix) + * Decimal(self._unit_time) ) assert isinstance(derivative, Decimal) except ValueError as err: diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 8893319ab36..05ce55223d0 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -38,26 +38,30 @@ async def test_state(hass): assert state.attributes.get("unit_of_measurement") == "kW" -async def test_dataSet1(hass): - """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } +async def _setup_sensor(hass, config): + default_config = { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "round": 2, } + config = {"sensor": dict(default_config, **config)} assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] hass.states.async_set(entity_id, 0, {}) await hass.async_block_till_done() + return config, entity_id + + +async def setup_tests(hass, config, times, values, expected_state): + """Test derivative sensor state.""" + config, entity_id = await _setup_sensor(hass, config) + # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + for time, value in zip(times, values): now = dt_util.utcnow() + timedelta(seconds=time) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set(entity_id, value, {}, force_update=True) @@ -66,163 +70,88 @@ async def test_dataSet1(hass): state = hass.states.get("sensor.power") assert state is not None - assert round(float(state.state), config["sensor"]["round"]) == -0.5 + assert round(float(state.state), config["sensor"]["round"]) == expected_state + + return state + + +async def test_dataSet1(hass): + """Test derivative sensor state.""" + await setup_tests( + hass, + {"unit_time": "s"}, + times=[20, 30, 40, 50], + values=[10, 30, 5, 0], + expected_state=-0.5, + ) async def test_dataSet2(hass): """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - assert await async_setup_component(hass, "sensor", config) - - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {}) - await hass.async_block_till_done() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 5), (30, 0)]: - now = dt_util.utcnow() + 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() - - state = hass.states.get("sensor.power") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == -0.5 + await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[5, 0], expected_state=-0.5 + ) async def test_dataSet3(hass): """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - assert await async_setup_component(hass, "sensor", config) - - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {}) - await hass.async_block_till_done() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 5), (30, 10)]: - now = dt_util.utcnow() + 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() - - state = hass.states.get("sensor.power") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 0.5 + state = await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[5, 10], expected_state=0.5 + ) assert state.attributes.get("unit_of_measurement") == "/s" async def test_dataSet4(hass): """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - assert await async_setup_component(hass, "sensor", config) - - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {}) - await hass.async_block_till_done() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 5), (30, 5)]: - now = dt_util.utcnow() + 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() - - state = hass.states.get("sensor.power") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 0 + await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[5, 5], expected_state=0 + ) async def test_dataSet5(hass): """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - assert await async_setup_component(hass, "sensor", config) - - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {}) - await hass.async_block_till_done() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 10), (30, -10)]: - now = dt_util.utcnow() + 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() - - state = hass.states.get("sensor.power") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == -2 + await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[10, -10], expected_state=-2 + ) async def test_dataSet6(hass): """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "round": 2, - } - } + await setup_tests(hass, {}, times=[0, 60], values=[0, 1 / 60], expected_state=1) - assert await async_setup_component(hass, "sensor", config) - entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {}) - await hass.async_block_till_done() +async def test_data_moving_average_for_discrete_sensor(hass): + """Test derivative sensor state.""" + # We simulate the following situation: + # The temperature rises 1 °C per minute for 1 hour long. + # There is a data point every second, however, the sensor returns + # the temperature rounded down to an integer value. + # We use a time window of 10 minutes and therefore we can expect + # (because the true derivative is 1 °C/min) an error of less than 10%. - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 0), (30, 36000)]: + temperature_values = [] + for temperature in range(60): + temperature_values += [temperature] * 60 + time_window = 600 + + times = list(range(len(temperature_values))) + config, entity_id = await _setup_sensor( + hass, {"time_window": {"seconds": time_window}, "unit_time": "min", "round": 1} + ) # two minute window + + for time, value in zip(times, temperature_values): now = dt_util.utcnow() + 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() - state = hass.states.get("sensor.power") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 1 + if time_window < time < times[-1] - time_window: + state = hass.states.get("sensor.power") + derivative = round(float(state.state), config["sensor"]["round"]) + # Test that the error is never more than + # (time_window_in_minutes / true_derivative * 100) = 10% + assert abs(1 - derivative) <= 0.1 async def test_prefix(hass): From 4602d7370c3a0f7067c2a40c70bc18a5f7f0121e Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 3 Feb 2020 19:19:40 -0500 Subject: [PATCH 099/378] Remove Alexa.InputController from devices without supported inputs in Alexa (#31450) * Yield Alexa.InputController only for supported inputs. * Add Comment. * Comment fix. --- .../components/alexa/capabilities.py | 7 ++++ homeassistant/components/alexa/entities.py | 8 +++- tests/components/alexa/test_smart_home.py | 37 ++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index eba59e92cd4..94cf41d530b 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -749,6 +749,13 @@ class AlexaInputController(AlexaCapability): source_list = self.entity.attributes.get( media_player.ATTR_INPUT_SOURCE_LIST, [] ) + input_list = AlexaInputController.get_valid_inputs(source_list) + + return input_list + + @staticmethod + def get_valid_inputs(source_list): + """Return list of supported inputs.""" input_list = [] for source in source_list: formatted_source = ( diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 8801936b35b..b10f11e2bbc 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -526,7 +526,13 @@ class MediaPlayerCapabilities(AlexaEntity): yield AlexaSeekController(self.entity) if supported & media_player.SUPPORT_SELECT_SOURCE: - yield AlexaInputController(self.entity) + inputs = AlexaInputController.get_valid_inputs( + self.entity.attributes.get( + media_player.const.ATTR_INPUT_SOURCE_LIST, [] + ) + ) + if len(inputs) > 0: + yield AlexaInputController(self.entity) if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3e8e87f62c9..a714b69461c 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -887,6 +887,7 @@ async def test_media_player(hass): | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, "volume_level": 0.75, + "source_list": ["hdmi", "tv"], }, ) appliance = await discovery_test(device, hass) @@ -1047,7 +1048,6 @@ async def test_media_player_power(hass): "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", - "Alexa.InputController", "Alexa.PlaybackController", "Alexa.PlaybackStateReporter", "Alexa.PowerController", @@ -1176,6 +1176,41 @@ async def test_media_player_inputs(hass): assert call.data["source"] == "tv" +async def test_media_player_no_supported_inputs(hass): + """Test media player discovery with no supported inputs.""" + device = ( + "media_player.test_no_inputs", + "off", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOURCE, + "volume_level": 0.75, + "source_list": [ + "foo", + "foo_2", + "vcr", + "betamax", + "record_player", + "f.m.", + "a.m.", + "tape_deck", + "laser_disc", + "hd_dvd", + ], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test_no_inputs" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player" + + # Assert Alexa.InputController is not in capabilities list. + assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.EndpointHealth", "Alexa.PowerController" + ) + + async def test_media_player_speaker(hass): """Test media player with speaker interface.""" device = ( From db6449c3fb8672050d4db89a49138baebac071d8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 4 Feb 2020 00:31:49 +0000 Subject: [PATCH 100/378] [ci skip] Translation update --- .../components/glances/.translations/hu.json | 37 +++++++++++++++++++ .../samsungtv/.translations/en.json | 10 +++-- 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/glances/.translations/hu.json diff --git a/homeassistant/components/glances/.translations/hu.json b/homeassistant/components/glances/.translations/hu.json new file mode 100644 index 00000000000..1d7c2ea4023 --- /dev/null +++ b/homeassistant/components/glances/.translations/hu.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van." + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni a kiszolg\u00e1l\u00f3hoz", + "wrong_version": "Nem t\u00e1mogatott verzi\u00f3 (2 vagy 3 csak)" + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "ssl": "Az SSL / TLS haszn\u00e1lat\u00e1val csatlakozzon a Glances rendszerhez", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "A rendszer tan\u00fas\u00edt\u00e1s\u00e1nak ellen\u0151rz\u00e9se", + "version": "Glances API-verzi\u00f3 (2 vagy 3)" + }, + "title": "Glances Be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" + }, + "description": "A Glances be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/en.json b/homeassistant/components/samsungtv/.translations/en.json index 24ab81c007c..2d3856fbaff 100644 --- a/homeassistant/components/samsungtv/.translations/en.json +++ b/homeassistant/components/samsungtv/.translations/en.json @@ -3,13 +3,15 @@ "abort": { "already_configured": "This Samsung TV is already configured.", "already_in_progress": "Samsung TV configuration is already in progress.", - "auth_missing": "Home Assistant is not authenticated to connect to this Samsung TV.", + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", "not_found": "No supported Samsung TV devices found on the network.", - "not_supported": "This Samsung TV devices is currently not supported." + "not_successful": "Unable to connect to this Samsung TV device.", + "not_supported": "This Samsung TV device is currently not supported." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authentication. Manual configurations for this TV will be overwritten.", + "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten.", "title": "Samsung TV" }, "user": { @@ -17,7 +19,7 @@ "host": "Host or IP address", "name": "Name" }, - "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authentication.", + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", "title": "Samsung TV" } }, From 151f60658c3b677432872f715950780639fe9024 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 4 Feb 2020 03:19:14 -0500 Subject: [PATCH 101/378] Bump pyvizio version for bug fixes (#31453) --- 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 6a5b9c33111..bf88ed9f437 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.19"], + "requirements": ["pyvizio==0.1.21"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index cd6c0647f46..7493fd9bbda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.19 +pyvizio==0.1.21 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f0e6a21a2a..85d80087ae6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.19 +pyvizio==0.1.21 # homeassistant.components.html5 pywebpush==1.9.2 From 9097912469532d69f78b4a21549f2d92de99536f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Feb 2020 17:07:09 +0100 Subject: [PATCH 102/378] Updated frontend to 20200130.1 (#31460) --- 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 6b16970c675..09bd35ba89b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200130.0" + "home-assistant-frontend==20200130.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e646381c966..3e4c1cf581b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200130.0 +home-assistant-frontend==20200130.1 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7493fd9bbda..b25ca1e3f05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.0 +home-assistant-frontend==20200130.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85d80087ae6..d0958d98140 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.0 +home-assistant-frontend==20200130.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From 8af9585f12e59a625097dbab5d07e37b92a4eb4c Mon Sep 17 00:00:00 2001 From: etheralm <8655564+etheralm@users.noreply.github.com> Date: Tue, 4 Feb 2020 17:23:08 +0100 Subject: [PATCH 103/378] Update libpurecool upstream library to latest version (#31457) * Update upstream library to latest version * update version in requirements_all.txt * update version in requirements_all.txt --- homeassistant/components/dyson/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 4fc49b4ca60..f6c0c187c8c 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.0"], + "requirements": ["libpurecool==0.6.1"], "dependencies": [], "codeowners": ["@etheralm"] } diff --git a/requirements_all.txt b/requirements_all.txt index b25ca1e3f05..a427826d861 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,7 +766,7 @@ konnected==0.1.5 lakeside==0.12 # homeassistant.components.dyson -libpurecool==0.6.0 +libpurecool==0.6.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0958d98140..527a0eae504 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ keyring==20.0.0 keyrings.alt==3.4.0 # homeassistant.components.dyson -libpurecool==0.6.0 +libpurecool==0.6.1 # homeassistant.components.mikrotik librouteros==3.0.0 From 1efea50654958dcf70885dc534c19620fa0ee4ea Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 4 Feb 2020 14:31:03 -0500 Subject: [PATCH 104/378] Update vizio host check to handle entries that don't have port (#31463) * Update vizio host check to handle entries that don't have port * add comment explaining no_port test for future * remove _name_is_same function and support user updating name in config * Update strings.json Co-authored-by: Paulus Schoutsen --- homeassistant/components/vizio/config_flow.py | 30 +++++++--- homeassistant/components/vizio/strings.json | 4 +- tests/components/vizio/test_config_flow.py | 56 ++++++++++++++++++- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 5500ec3db94..04f70da4a8c 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -53,6 +53,11 @@ def _get_config_flow_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: ) +def _host_is_same(host1: str, host2: str) -> bool: + """Check if host1 and host2 are the same.""" + return host1.split(":")[0] == host2.split(":")[0] + + class VizioOptionsConfigFlow(config_entries.OptionsFlow): """Handle Transmission client options.""" @@ -108,7 +113,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if new config entry matches any existing config entries for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: + if _host_is_same(entry.data[CONF_HOST], user_input[CONF_HOST]): errors[CONF_HOST] = "host_exists" break @@ -165,24 +170,31 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry from configuration.yaml.""" # Check if new config entry matches any existing config entries for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == import_config[CONF_HOST] and entry.data[ - CONF_NAME - ] == import_config.get(CONF_NAME): + if _host_is_same(entry.data[CONF_HOST], import_config[CONF_HOST]): updated_options = {} + updated_name = {} + + if entry.data[CONF_NAME] != import_config[CONF_NAME]: + updated_name[CONF_NAME] = import_config[CONF_NAME] if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]: updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] - if updated_options: + if updated_options or updated_name: new_data = entry.data.copy() - new_data.update(updated_options) new_options = entry.options.copy() - new_options.update(updated_options) + + if updated_name: + new_data.update(updated_name) + + if updated_options: + new_data.update(updated_options) + new_options.update(updated_options) self.hass.config_entries.async_update_entry( entry=entry, data=new_data, options=new_options, ) - return self.async_abort(reason="updated_options") + return self.async_abort(reason="updated_entry") return self.async_abort(reason="already_setup") @@ -199,7 +211,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if new config entry matches any existing config entries and abort if so for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == discovery_info[CONF_HOST]: + if _host_is_same(entry.data[CONF_HOST], discovery_info[CONF_HOST]): return self.async_abort(reason="already_setup") # Set default name to discovered device name by stripping zeroconf service diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 305e49d56f8..64b2fb5f936 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -21,7 +21,7 @@ "abort": { "already_setup": "This entry has already been setup.", "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", - "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly." + "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly." } }, "options": { @@ -35,4 +35,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 41da5c0267b..069c00bf6b2 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -130,6 +130,30 @@ async def test_user_host_already_configured( assert result["errors"] == {CONF_HOST: "host_exists"} +async def test_user_host_already_configured_no_port( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test host is already configured during user setup when existing entry has no port.""" + # Mock entry without port so we can test that the same entry WITH a port will fail + no_port_entry = MOCK_SPEAKER_CONFIG.copy() + no_port_entry[CONF_HOST] = no_port_entry[CONF_HOST].split(":")[0] + entry = MockConfigEntry( + domain=DOMAIN, data=no_port_entry, options={CONF_VOLUME_STEP: VOLUME_STEP} + ) + entry.add_to_hass(hass) + fail_entry = MOCK_SPEAKER_CONFIG.copy() + fail_entry[CONF_NAME] = "newtestname" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "host_exists"} + + async def test_user_name_already_configured( hass: HomeAssistantType, vizio_connect: pytest.fixture, @@ -293,13 +317,43 @@ async def test_import_flow_update_options( ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "updated_options" + assert result["reason"] == "updated_entry" assert ( hass.config_entries.async_get_entry(entry_id).options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 ) +async def test_import_flow_update_name( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: + """Test import config flow with updated name.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), + ) + await hass.async_block_till_done() + + assert result["result"].data[CONF_NAME] == NAME + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry_id = result["result"].entry_id + + updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() + updated_config[CONF_NAME] = NAME2 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(updated_config), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + assert hass.config_entries.async_get_entry(entry_id).data[CONF_NAME] == NAME2 + + async def test_zeroconf_flow( hass: HomeAssistantType, vizio_connect: pytest.fixture, From 201ea2557ef2600cfdc55adaa67b3b668d180c05 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 4 Feb 2020 22:37:59 +0100 Subject: [PATCH 105/378] Add config flow to Meteo-France (#29927) * Add config flow to Meteo-France * Review 1 * Use config_entry.unique_id * Fix config_flow _show_setup_form + init * Remove empty *_setup_platform() * Avoid HomeAssistantError: Entity id already exists: sensor.[city_name]_[sensor_type]. Platform meteo_france does not generate unique IDs - when multiple district in one city * Review + abort when API error * Fix I/O * Remove monitored_conditions * Add async_unload_entry * Review 3 * Fix pipe * alert_watcher is already None * Review 4 * Better fix for "Entity id already exists" * Whoops, fix tests * Fix string --- .coveragerc | 5 +- CODEOWNERS | 2 +- .../meteo_france/.translations/en.json | 18 +++ .../components/meteo_france/__init__.py | 151 ++++++++---------- .../components/meteo_france/config_flow.py | 62 +++++++ .../components/meteo_france/const.py | 2 +- .../components/meteo_france/manifest.json | 3 +- .../components/meteo_france/sensor.py | 121 +++++++------- .../components/meteo_france/strings.json | 18 +++ .../components/meteo_france/weather.py | 26 +-- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 6 + tests/components/meteo_france/__init__.py | 1 + .../meteo_france/test_config_flow.py | 128 +++++++++++++++ 14 files changed, 389 insertions(+), 155 deletions(-) create mode 100644 homeassistant/components/meteo_france/.translations/en.json create mode 100644 homeassistant/components/meteo_france/config_flow.py create mode 100644 homeassistant/components/meteo_france/strings.json create mode 100644 tests/components/meteo_france/__init__.py create mode 100644 tests/components/meteo_france/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index cc2402e480b..8fe53ec8282 100644 --- a/.coveragerc +++ b/.coveragerc @@ -413,7 +413,10 @@ omit = homeassistant/components/mediaroom/media_player.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py - homeassistant/components/meteo_france/* + homeassistant/components/meteo_france/__init__.py + homeassistant/components/meteo_france/const.py + homeassistant/components/meteo_france/sensor.py + homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py diff --git a/CODEOWNERS b/CODEOWNERS index 5a66f80a1d0..4df7f250d60 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,7 +206,7 @@ homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mediaroom/* @dgomes homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen -homeassistant/components/meteo_france/* @victorcerutti @oncleben31 +homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mikrotik/* @engrbm87 diff --git a/homeassistant/components/meteo_france/.translations/en.json b/homeassistant/components/meteo_france/.translations/en.json new file mode 100644 index 00000000000..804ad9d67b1 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "City already configured", + "unknown": "Unknown error: please retry later" + }, + "step": { + "user": { + "data": { + "city": "City" + }, + "description": "Enter the postal code (only for France, recommended) or city name", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 73b8dbb0e39..b7eda51b955 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,4 +1,5 @@ """Support for Meteo-France weather data.""" +import asyncio import datetime import logging @@ -6,116 +7,96 @@ from meteofrance.client import meteofranceClient, meteofranceError from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy import voluptuous as vol -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle -from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES +from .const import CONF_CITY, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=5) -def has_all_unique_cities(value): - """Validate that all cities are unique.""" - cities = [location[CONF_CITY] for location in value] - vol.Schema(vol.Unique())(cities) - return value - +CITY_SCHEMA = vol.Schema({vol.Required(CONF_CITY): cv.string}) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_CITY): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } - ) - ], - has_all_unique_cities, - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) -def setup(hass, config): - """Set up the Meteo-France component.""" - hass.data[DATA_METEO_FRANCE] = {} +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up Meteo-France from legacy config file.""" - # Check if at least weather alert have to be monitored for one location. - need_weather_alert_watcher = False - for location in config[DOMAIN]: - if ( - CONF_MONITORED_CONDITIONS in location - and "weather_alert" in location[CONF_MONITORED_CONDITIONS] - ): - need_weather_alert_watcher = True + conf = config.get(DOMAIN) + if conf is None: + return True - # If weather alert monitoring is expected initiate a client to be used by - # all weather_alert entities. - if need_weather_alert_watcher: - _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") - - weather_alert_client = VigilanceMeteoFranceProxy() - try: - weather_alert_client.update_data() - except VigilanceMeteoError as exp: - _LOGGER.error( - "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", - exp, + for city_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=city_conf.copy() ) - else: - weather_alert_client = None - hass.data[DATA_METEO_FRANCE]["weather_alert_client"] = weather_alert_client - - for location in config[DOMAIN]: - - city = location[CONF_CITY] - - try: - client = meteofranceClient(city) - except meteofranceError as exp: - _LOGGER.error( - "Unexpected error when creating the meteofrance proxy: %s", exp - ) - return - - client.need_rain_forecast = bool( - CONF_MONITORED_CONDITIONS in location - and "next_rain" in location[CONF_MONITORED_CONDITIONS] ) - hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) - hass.data[DATA_METEO_FRANCE][city].update() - - if CONF_MONITORED_CONDITIONS in location: - monitored_conditions = location[CONF_MONITORED_CONDITIONS] - _LOGGER.debug("meteo_france sensor platform loaded for %s", city) - load_platform( - hass, - "sensor", - DOMAIN, - {CONF_CITY: city, CONF_MONITORED_CONDITIONS: monitored_conditions}, - config, - ) - - load_platform(hass, "weather", DOMAIN, {CONF_CITY: city}, config) - return True +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an Meteo-France account from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + # Weather alert + weather_alert_client = VigilanceMeteoFranceProxy() + try: + await hass.async_add_executor_job(weather_alert_client.update_data) + except VigilanceMeteoError as exp: + _LOGGER.error( + "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", exp + ) + return False + hass.data[DOMAIN]["weather_alert_client"] = weather_alert_client + + # Weather + city = entry.data[CONF_CITY] + try: + client = await hass.async_add_executor_job(meteofranceClient, city) + except meteofranceError as exp: + _LOGGER.error("Unexpected error when creating the meteofrance proxy: %s", exp) + return False + + hass.data[DOMAIN][city] = MeteoFranceUpdater(client) + await hass.async_add_executor_job(hass.data[DOMAIN][city].update) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + _LOGGER.debug("meteo_france sensor platform loaded for %s", city) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.data[CONF_CITY]) + + return unload_ok + + class MeteoFranceUpdater: """Update data from Meteo-France.""" - def __init__(self, client): + def __init__(self, client: meteofranceClient): """Initialize the data object.""" self._client = client diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py new file mode 100644 index 00000000000..c7673020360 --- /dev/null +++ b/homeassistant/components/meteo_france/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow to configure the Meteo-France integration.""" +import logging + +from meteofrance.client import meteofranceClient, meteofranceError +import voluptuous as vol + +from homeassistant import config_entries + +from .const import CONF_CITY +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Meteo-France config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_CITY, default=user_input.get(CONF_CITY, "")): str} + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, errors) + + city = user_input[CONF_CITY] # Might be a city name or a postal code + city_name = None + + try: + client = await self.hass.async_add_executor_job(meteofranceClient, city) + city_name = client.get_data()["name"] + except meteofranceError as exp: + _LOGGER.error( + "Unexpected error when creating the meteofrance proxy: %s", exp + ) + return self.async_abort(reason="unknown") + + # Check if already configured + await self.async_set_unique_id(city_name) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=city_name, data={CONF_CITY: city}) + + async def async_step_import(self, user_input): + """Import a config entry.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 223aca20bac..fae2000b19a 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -3,7 +3,7 @@ from homeassistant.const import TEMP_CELSIUS DOMAIN = "meteo_france" -DATA_METEO_FRANCE = "data_meteo_france" +PLATFORMS = ["sensor", "weather"] ATTRIBUTION = "Data provided by Météo-France" CONF_CITY = "city" diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 41a003ea4f7..77f8fca984d 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,8 +1,9 @@ { "domain": "meteo_france", "name": "Météo-France", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.0"], "dependencies": [], - "codeowners": ["@victorcerutti", "@oncleben31"] + "codeowners": ["@victorcerutti", "@oncleben31", "@Quentame"] } diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index f0c08ac1822..cf28b9ea558 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,15 +1,18 @@ """Support for Meteo-France raining forecast sensor.""" import logging -from vigilancemeteo import DepartmentWeatherAlert +from meteofrance.client import meteofranceClient +from vigilancemeteo import DepartmentWeatherAlert, VigilanceMeteoFranceProxy -from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTRIBUTION, CONF_CITY, - DATA_METEO_FRANCE, + DOMAIN, SENSOR_TYPE_CLASS, SENSOR_TYPE_ICON, SENSOR_TYPE_NAME, @@ -23,52 +26,47 @@ STATE_ATTR_FORECAST = "1h rain forecast" STATE_ATTR_BULLETIN_TIME = "Bulletin date" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Meteo-France sensor.""" - if discovery_info is None: - return - - city = discovery_info[CONF_CITY] - monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] - client = hass.data[DATA_METEO_FRANCE][city] - weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"] +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Meteo-France sensor platform.""" + city = entry.data[CONF_CITY] + client = hass.data[DOMAIN][city] + weather_alert_client = hass.data[DOMAIN]["weather_alert_client"] alert_watcher = None - if "weather_alert" in monitored_conditions: - datas = hass.data[DATA_METEO_FRANCE][city].get_data() - # Check if a department code is available for this city. - if "dept" in datas: - try: - # If yes create the watcher DepartmentWeatherAlert object. - alert_watcher = DepartmentWeatherAlert( - datas["dept"], weather_alert_client - ) - except ValueError as exp: - _LOGGER.error( - "Unexpected error when creating the weather alert sensor for %s in department %s: %s", - city, - datas["dept"], - exp, - ) - alert_watcher = None - else: - _LOGGER.info( - "Weather alert watcher added for %s in department %s", - city, - datas["dept"], - ) - else: - _LOGGER.warning( - "No 'dept' key found for '%s'. So weather alert information won't be available", - city, + datas = client.get_data() + # Check if a department code is available for this city. + if "dept" in datas: + try: + # If yes create the watcher DepartmentWeatherAlert object. + alert_watcher = await hass.async_add_executor_job( + DepartmentWeatherAlert, datas["dept"], weather_alert_client ) - # Exit and don't create the sensor if no department code available. - return + _LOGGER.info( + "Weather alert watcher added for %s in department %s", + city, + datas["dept"], + ) + except ValueError as exp: + _LOGGER.error( + "Unexpected error when creating the weather alert sensor for %s in department %s: %s", + city, + datas["dept"], + exp, + ) + else: + _LOGGER.warning( + "No 'dept' key found for '%s'. So weather alert information won't be available", + city, + ) + # Exit and don't create the sensor if no department code available. + return - add_entities( + async_add_entities( [ - MeteoFranceSensor(variable, client, alert_watcher) - for variable in monitored_conditions + MeteoFranceSensor(sensor_type, client, alert_watcher) + for sensor_type in SENSOR_TYPES ], True, ) @@ -77,9 +75,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MeteoFranceSensor(Entity): """Representation of a Meteo-France sensor.""" - def __init__(self, condition, client, alert_watcher): + def __init__( + self, + sensor_type: str, + client: meteofranceClient, + alert_watcher: VigilanceMeteoFranceProxy, + ): """Initialize the Meteo-France sensor.""" - self._condition = condition + self._type = sensor_type self._client = client self._alert_watcher = alert_watcher self._state = None @@ -88,7 +91,12 @@ class MeteoFranceSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}" + return f"{self._data['name']} {SENSOR_TYPES[self._type][SENSOR_TYPE_NAME]}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self.name @property def state(self): @@ -99,7 +107,7 @@ class MeteoFranceSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" # Attributes for next_rain sensor. - if self._condition == "next_rain" and "rain_forecast" in self._data: + if self._type == "next_rain" and "rain_forecast" in self._data: return { **{STATE_ATTR_FORECAST: self._data["rain_forecast"]}, **self._data["next_rain_intervals"], @@ -107,7 +115,7 @@ class MeteoFranceSensor(Entity): } # Attributes for weather_alert sensor. - if self._condition == "weather_alert" and self._alert_watcher is not None: + if self._type == "weather_alert" and self._alert_watcher is not None: return { **{STATE_ATTR_BULLETIN_TIME: self._alert_watcher.bulletin_date}, **self._alert_watcher.alerts_list, @@ -120,17 +128,17 @@ class MeteoFranceSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT] + return SENSOR_TYPES[self._type][SENSOR_TYPE_UNIT] @property def icon(self): """Return the icon.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON] + return SENSOR_TYPES[self._type][SENSOR_TYPE_ICON] @property def device_class(self): """Return the device class of the sensor.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS] + return SENSOR_TYPES[self._type][SENSOR_TYPE_CLASS] def update(self): """Fetch new state data for the sensor.""" @@ -138,13 +146,12 @@ class MeteoFranceSensor(Entity): self._client.update() self._data = self._client.get_data() - if self._condition == "weather_alert": + if self._type == "weather_alert": if self._alert_watcher is not None: self._alert_watcher.update_department_status() self._state = self._alert_watcher.department_color _LOGGER.debug( - "weather alert watcher for %s updated. Proxy" - " have the status: %s", + "weather alert watcher for %s updated. Proxy have the status: %s", self._data["name"], self._alert_watcher.proxy.status, ) @@ -153,9 +160,9 @@ class MeteoFranceSensor(Entity): "No weather alert data for location %s", self._data["name"] ) else: - self._state = self._data[self._condition] + self._state = self._data[self._type] except KeyError: _LOGGER.error( - "No condition %s for location %s", self._condition, self._data["name"] + "No condition %s for location %s", self._type, self._data["name"] ) self._state = None diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json new file mode 100644 index 00000000000..8bb02f28bd0 --- /dev/null +++ b/homeassistant/components/meteo_france/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Météo-France", + "step": { + "user": { + "title": "Météo-France", + "description": "Enter the postal code (only for France, recommended) or city name", + "data": { + "city": "City" + } + } + }, + "abort":{ + "already_configured": "City already configured", + "unknown": "Unknown error: please retry later" + } + } +} diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index c96080808e9..1bdea073aae 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from meteofrance.client import meteofranceClient + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -9,29 +11,30 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, WeatherEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util -from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE +from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the Meteo-France weather platform.""" - if discovery_info is None: - return + city = entry.data[CONF_CITY] + client = hass.data[DOMAIN][city] - city = discovery_info[CONF_CITY] - client = hass.data[DATA_METEO_FRANCE][city] - - add_entities([MeteoFranceWeather(client)], True) + async_add_entities([MeteoFranceWeather(client)], True) class MeteoFranceWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, client): + def __init__(self, client: meteofranceClient): """Initialise the platform with a data instance and station name.""" self._client = client self._data = {} @@ -46,6 +49,11 @@ class MeteoFranceWeather(WeatherEntity): """Return the name of the sensor.""" return self._data["name"] + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self.name + @property def condition(self): """Return the current condition.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf77dae7fb2..ea8a0a4e82d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -54,6 +54,7 @@ FLOWS = [ "luftdaten", "mailgun", "met", + "meteo_france", "mikrotik", "mobile_app", "mqtt", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 527a0eae504..904df51f68f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,6 +298,9 @@ luftdaten==0.6.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 +# homeassistant.components.meteo_france +meteofrance==0.3.7 + # homeassistant.components.mfi mficlient==0.3.0 @@ -663,6 +666,9 @@ url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 +# homeassistant.components.meteo_france +vigilancemeteo==3.0.0 + # homeassistant.components.verisure vsure==1.5.4 diff --git a/tests/components/meteo_france/__init__.py b/tests/components/meteo_france/__init__.py new file mode 100644 index 00000000000..c4d4c446574 --- /dev/null +++ b/tests/components/meteo_france/__init__.py @@ -0,0 +1 @@ +"""Tests for the Meteo-France component.""" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py new file mode 100644 index 00000000000..f9ead2c1ef3 --- /dev/null +++ b/tests/components/meteo_france/test_config_flow.py @@ -0,0 +1,128 @@ +"""Tests for the Meteo-France config flow.""" +from unittest.mock import patch + +from meteofrance.client import meteofranceError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER + +from tests.common import MockConfigEntry + +CITY_1_POSTAL = "74220" +CITY_1_NAME = "La Clusaz" +CITY_2_POSTAL_DISTRICT_1 = "69001" +CITY_2_POSTAL_DISTRICT_4 = "69004" +CITY_2_NAME = "Lyon" + + +@pytest.fixture(name="client_1") +def mock_controller_client_1(): + """Mock a successful client.""" + with patch( + "homeassistant.components.meteo_france.config_flow.meteofranceClient", + update=False, + ) as service_mock: + service_mock.return_value.get_data.return_value = {"name": CITY_1_NAME} + yield service_mock + + +@pytest.fixture(name="client_2") +def mock_controller_client_2(): + """Mock a successful client.""" + with patch( + "homeassistant.components.meteo_france.config_flow.meteofranceClient", + update=False, + ) as service_mock: + service_mock.return_value.get_data.return_value = {"name": CITY_2_NAME} + yield service_mock + + +async def test_user(hass, client_1): + """Test user config.""" + 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" + + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == CITY_1_NAME + assert result["title"] == CITY_1_NAME + assert result["data"][CONF_CITY] == CITY_1_POSTAL + + +async def test_import(hass, client_1): + """Test import step.""" + # import with all + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == CITY_1_NAME + assert result["title"] == CITY_1_NAME + assert result["data"][CONF_CITY] == CITY_1_POSTAL + + +async def test_abort_if_already_setup(hass, client_1): + """Test we abort if already setup.""" + MockConfigEntry( + domain=DOMAIN, data={CONF_CITY: CITY_1_POSTAL}, unique_id=CITY_1_NAME + ).add_to_hass(hass) + + # Should fail, same CITY same postal code (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same CITY same postal code (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_if_already_setup_district(hass, client_2): + """Test we abort if already setup.""" + MockConfigEntry( + domain=DOMAIN, data={CONF_CITY: CITY_2_POSTAL_DISTRICT_1}, unique_id=CITY_2_NAME + ).add_to_hass(hass) + + # Should fail, same CITY different postal code (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_CITY: CITY_2_POSTAL_DISTRICT_4}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same CITY different postal code (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_CITY: CITY_2_POSTAL_DISTRICT_4}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_client_failed(hass): + """Test when we have errors during client fetch.""" + with patch( + "homeassistant.components.meteo_france.config_flow.meteofranceClient", + side_effect=meteofranceError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From f41623ca6423ac2e83b10801aac4d062f9f9b72f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 14:42:07 -0800 Subject: [PATCH 106/378] Log warning when entities referenced in service call not found (#31427) * Raise entities not found error * Make it a warning, not an error * Add support for MATCH_ENTITY_NONE * Fix lint * Fix tests --- homeassistant/components/amcrest/__init__.py | 4 + .../components/denonavr/media_player.py | 5 + homeassistant/components/insteon/schemas.py | 5 +- homeassistant/components/lifx/light.py | 4 + homeassistant/components/tts/__init__.py | 6 +- .../components/webostv/media_player.py | 4 + homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 9 +- homeassistant/helpers/service.py | 54 ++++++-- tests/components/google_translate/test_tts.py | 24 +++- tests/components/marytts/test_tts.py | 21 ++- tests/components/tts/test_init.py | 122 +++++++++++------- tests/components/voicerss/test_tts.py | 26 +++- tests/components/yandextts/test_tts.py | 39 ++++-- tests/helpers/test_entity_component.py | 72 ++++++++++- tests/helpers/test_service.py | 32 ++++- 16 files changed, 339 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f7814939e3a..d1e1aafa6f3 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, HTTP_BASIC_AUTHENTICATION, ) from homeassistant.exceptions import Unauthorized, UnknownUser @@ -236,6 +237,9 @@ def setup(hass, config): if have_permission(user, entity_id) ] + if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + call_ids = await async_extract_entity_ids(hass, call) entity_ids = [] for entity_id in hass.data[DATA_AMCREST][CAMERAS]: diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 350d065f9d9..b14592d1b78 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_ZONE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -201,6 +202,10 @@ class DenonDevice(MediaPlayerDevice): def signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" entity_ids = data[ATTR_ENTITY_ID] + + if entity_ids == ENTITY_MATCH_NONE: + return + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: params = { key: value diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 1ae4ebed99e..20399195365 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_PORT, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, ) import homeassistant.helpers.config_validation as cv @@ -136,7 +137,9 @@ DEL_ALL_LINK_SCHEMA = vol.Schema( LOAD_ALDB_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), + vol.Required(CONF_ENTITY_ID): vol.Any( + cv.entity_id, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE + ), vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, } ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 4e845a07854..5bc0c1bc53b 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback @@ -374,6 +375,9 @@ class LIFXManager: async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: return self.entities.values() diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 318101605e8..3a456dec531 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -22,7 +22,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -90,7 +90,7 @@ SCHEMA_SERVICE_SAY = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_CACHE): cv.boolean, - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_LANGUAGE): cv.string, vol.Optional(ATTR_OPTIONS): dict, } @@ -148,7 +148,7 @@ async def async_setup(hass, config): async def async_say_handle(service): """Service handle for say.""" - entity_ids = service.data.get(ATTR_ENTITY_ID, ENTITY_MATCH_ALL) + entity_ids = service.data[ATTR_ENTITY_ID] message = service.data.get(ATTR_MESSAGE) cache = service.data.get(ATTR_CACHE) language = service.data.get(ATTR_LANGUAGE) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 99df9fd17ce..f4d9f97fe42 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, STATE_OFF, STATE_ON, ) @@ -137,6 +138,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_NONE: + return + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: params = { key: value diff --git a/homeassistant/const.py b/homeassistant/const.py index facb365f75c..ee2c4767ba9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,6 +16,7 @@ PLATFORM_FORMAT = "{platform}.{domain}" MATCH_ALL = "*" # Entity target all constant +ENTITY_MATCH_NONE = "none" ENTITY_MATCH_ALL = "all" # If no name is specified diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 852948220de..f1caf38bf8b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -52,6 +52,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, TEMP_CELSIUS, @@ -231,7 +232,9 @@ def entity_ids(value: Union[str, List]) -> List[str]: return [entity_id(ent_id) for ent_id in value] -comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids) +comp_entity_ids = vol.Any( + vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), entity_ids +) def entity_domain(domain: str) -> Callable[[Any], str]: @@ -736,7 +739,9 @@ def make_entity_service_schema( { **schema, vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), + vol.Optional(ATTR_AREA_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) + ), }, extra=extra, ), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index b30cab3fbd4..51f881181af 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -7,7 +7,12 @@ from typing import Callable import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL -from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, +) import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, @@ -121,11 +126,25 @@ async def async_extract_entities(hass, entities, service_call, expand_group=True entity_ids = await async_extract_entity_ids(hass, service_call, expand_group) - return [ - entity - for entity in entities - if entity.available and entity.entity_id in entity_ids - ] + found = [] + + for entity in entities: + if entity.entity_id not in entity_ids: + continue + + entity_ids.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)) + ) + + return found @bind_hass @@ -137,12 +156,15 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): entity_ids = service_call.data.get(ATTR_ENTITY_ID) area_ids = service_call.data.get(ATTR_AREA_ID) - if not entity_ids and not area_ids: - return [] - extracted = set() - if entity_ids: + if entity_ids in (None, ENTITY_MATCH_NONE) and area_ids in ( + None, + ENTITY_MATCH_NONE, + ): + return extracted + + if entity_ids and entity_ids != ENTITY_MATCH_NONE: # Entity ID attr can be a list or a string if isinstance(entity_ids, str): entity_ids = [entity_ids] @@ -152,7 +174,7 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): extracted.update(entity_ids) - if area_ids: + if area_ids and area_ids != ENTITY_MATCH_NONE: if isinstance(area_ids, str): area_ids = [area_ids] @@ -342,6 +364,16 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non platforms_entities.append(platform_entities) + if not target_all_entities: + for platform_entities in platforms_entities: + for entity in platform_entities: + entity_ids.remove(entity.entity_id) + + if entity_ids: + _LOGGER.warning( + "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) + ) + tasks = [ _handle_service_platform_call( hass, func, data, entities, call.context, required_features diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 15e84b384c0..37609e151bd 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -65,7 +65,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -89,7 +92,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -115,6 +121,7 @@ class TestTTSGooglePlatform: 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", }, @@ -139,7 +146,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -161,7 +171,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -193,6 +206,7 @@ class TestTTSGooglePlatform: 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." @@ -203,7 +217,7 @@ class TestTTSGooglePlatform: "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() diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 810998ec0b8..d8a96b2db52 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -66,7 +66,12 @@ class TestTTSMaryTTSPlatform: with patch("http.client.HTTPConnection", return_value=conn): self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "marytts_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + }, ) self.hass.block_till_done() @@ -93,7 +98,12 @@ class TestTTSMaryTTSPlatform: with patch("http.client.HTTPConnection", return_value=conn): self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "marytts_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + }, ) self.hass.block_till_done() @@ -123,7 +133,12 @@ class TestTTSMaryTTSPlatform: with patch("http.client.HTTPConnection", return_value=conn): self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "marytts_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + }, ) self.hass.block_till_done() diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 6aafe29901d..62c4bc3a065 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -95,7 +95,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -103,13 +106,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -125,7 +128,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -133,13 +139,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", ) ) @@ -163,7 +169,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", }, ) @@ -173,13 +180,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", ) ) @@ -196,7 +203,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "lang", }, ) @@ -206,7 +214,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_lang_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3", ) ) @@ -223,7 +231,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", tts.ATTR_OPTIONS: {"voice": "alex"}, }, @@ -236,13 +245,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) @@ -265,7 +274,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", }, ) @@ -277,13 +287,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) @@ -302,7 +312,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", tts.ATTR_OPTIONS: {"speed": 1}, }, @@ -315,7 +326,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) @@ -333,7 +344,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -341,7 +355,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert ( calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord" - "/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" "_en_-_demo.mp3" ) @@ -357,7 +371,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -365,7 +382,7 @@ class TestTTS: assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -375,7 +392,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -393,7 +410,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -401,7 +421,7 @@ class TestTTS: req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) _, demo_data = self.demo_provider.get_tts_audio("bla", "en") demo_data = tts.SpeechManager.write_tags( - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", demo_data, self.demo_provider, "AI person is in front of your door.", @@ -425,7 +445,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -433,10 +456,10 @@ class TestTTS: req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) _, demo_data = self.demo_provider.get_tts_audio("bla", "de") demo_data = tts.SpeechManager.write_tags( - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", demo_data, self.demo_provider, - "I person is on front of your door.", + "There is someone at the door.", "de", None, ) @@ -453,7 +476,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" + "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -487,7 +510,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -495,7 +521,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -512,7 +538,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_CACHE: False, }, ) @@ -522,7 +549,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -533,7 +560,7 @@ class TestTTS: _, demo_data = self.demo_provider.get_tts_audio("bla", "en") cache_file = os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) os.mkdir(self.default_tts_cache) @@ -552,14 +579,17 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() assert len(calls) == 1 assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( self.hass.config.api.base_url ) @@ -579,7 +609,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -590,7 +623,7 @@ class TestTTS: _, demo_data = self.demo_provider.get_tts_audio("bla", "en") cache_file = os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) os.mkdir(self.default_tts_cache) @@ -605,7 +638,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" + "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -622,14 +655,15 @@ async def test_setup_component_and_web_get_url(hass, hass_client): client = await hass_client() url = "/api/tts_get_url" - data = {"platform": "demo", "message": "I person is on front of your door."} + data = {"platform": "demo", "message": "There is someone at the door."} req = await client.post(url, json=data) assert req.status == 200 response = await req.json() assert response.get("url") == ( - "{}/api/tts_proxy/265944c108cbb00b2a62" - "1be5930513e03a0bb2cd_en_-_demo.mp3".format(hass.config.api.base_url) + "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( + hass.config.api.base_url + ) ) tts_cache = hass.config.path(tts.DEFAULT_CACHE_DIR) @@ -646,7 +680,7 @@ async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): client = await hass_client() url = "/api/tts_get_url" - data = {"message": "I person is on front of your door."} + data = {"message": "There is someone at the door."} req = await client.post(url, json=data) assert req.status == 400 diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index d2a7197fe1a..a65201735ae 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -67,7 +67,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -97,7 +100,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -121,6 +127,7 @@ class TestTTSVoiceRSSPlatform: tts.DOMAIN, "voicerss_say", { + "entity_id": "media_player.something", tts.ATTR_MESSAGE: "I person is on front of your door.", tts.ATTR_LANGUAGE: "de-de", }, @@ -145,7 +152,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -167,7 +177,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -194,7 +207,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index edd5c058f12..182c629d795 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -67,7 +67,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -103,7 +105,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -135,7 +139,11 @@ class TestTTSYandexPlatform: self.hass.services.call( tts.DOMAIN, "yandextts_say", - {tts.ATTR_MESSAGE: "HomeAssistant", tts.ATTR_LANGUAGE: "ru-RU"}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + tts.ATTR_LANGUAGE: "ru-RU", + }, ) self.hass.block_till_done() @@ -165,7 +173,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -195,7 +205,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -230,7 +242,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -266,7 +280,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -298,7 +314,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -330,7 +348,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -362,6 +382,7 @@ class TestTTSYandexPlatform: tts.DOMAIN, "yandextts_say", { + "entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant", "options": {"emotion": "evil", "speed": 2}, }, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 41f84e65f6c..306402cd2b9 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -7,8 +7,9 @@ from unittest.mock import Mock, patch import asynctest import pytest +import voluptuous as vol -from homeassistant.const import ENTITY_MATCH_ALL +from homeassistant.const import ENTITY_MATCH_ALL, ENTITY_MATCH_NONE import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import discovery @@ -223,10 +224,21 @@ async def test_extract_from_service_fails_if_no_entity_id(hass): [MockEntity(name="test_1"), MockEntity(name="test_2")] ) - call = ha.ServiceCall("test", "service") - - assert [] == sorted( - ent.entity_id for ent in (await component.async_extract_from_service(call)) + assert ( + await component.async_extract_from_service(ha.ServiceCall("test", "service")) + == [] + ) + assert ( + await component.async_extract_from_service( + ha.ServiceCall("test", "service", {"entity_id": ENTITY_MATCH_NONE}) + ) + == [] + ) + assert ( + await component.async_extract_from_service( + ha.ServiceCall("test", "service", {"area_id": ENTITY_MATCH_NONE}) + ) + == [] ) @@ -429,3 +441,53 @@ async def test_extract_all_use_match_all(hass, caplog): assert ( "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text + + +async def test_register_entity_service(hass): + """Test not expanding a group.""" + entity = MockEntity(entity_id=f"{DOMAIN}.entity") + calls = [] + + @ha.callback + def appender(**kwargs): + calls.append(kwargs) + + entity.async_called_by_service = appender + + component = EntityComponent(_LOGGER, DOMAIN, hass) + await component.async_add_entities([entity]) + + component.async_register_entity_service( + "hello", {"some": str}, "async_called_by_service" + ) + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + "hello", + {"entity_id": entity.entity_id, "invalid": "data"}, + blocking=True, + ) + assert len(calls) == 0 + + await hass.services.async_call( + DOMAIN, "hello", {"entity_id": entity.entity_id, "some": "data"}, blocking=True + ) + assert len(calls) == 1 + assert calls[0] == {"some": "data"} + + await hass.services.async_call( + DOMAIN, "hello", {"entity_id": ENTITY_MATCH_ALL, "some": "data"}, blocking=True + ) + assert len(calls) == 2 + assert calls[1] == {"some": "data"} + + await hass.services.async_call( + DOMAIN, "hello", {"entity_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True + ) + assert len(calls) == 2 + + await hass.services.async_call( + DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True + ) + assert len(calls) == 2 diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 5585437867c..106fdfabf2d 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -12,7 +12,13 @@ import voluptuous as vol from homeassistant import core as ha, exceptions from homeassistant.auth.permissions import PolicyPermissions import homeassistant.components # noqa: F401 -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers import ( device_registry as dev_reg, entity_registry as ent_reg, @@ -252,6 +258,14 @@ async def test_extract_entity_ids(hass): hass, call, expand_group=False ) + assert ( + await service.async_extract_entity_ids( + hass, + ha.ServiceCall("light", "turn_on", {ATTR_ENTITY_ID: ENTITY_MATCH_NONE}), + ) + == set() + ) + async def test_extract_entity_ids_from_area(hass, area_mock): """Test extract_entity_ids method with areas.""" @@ -266,6 +280,13 @@ async def test_extract_entity_ids_from_area(hass, area_mock): "light.diff_area", } == await service.async_extract_entity_ids(hass, call) + assert ( + await service.async_extract_entity_ids( + hass, ha.ServiceCall("light", "turn_on", {"area_id": ENTITY_MATCH_NONE}) + ) + == set() + ) + @asyncio.coroutine def test_async_get_all_descriptions(hass): @@ -742,6 +763,15 @@ async def test_extract_from_service_available_device(hass): for ent in (await service.async_extract_entities(hass, entities, call_2)) ] + assert ( + await service.async_extract_entities( + hass, + entities, + ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_NONE},), + ) + == [] + ) + async def test_extract_from_service_empty_if_no_entity_id(hass): """Test the extraction from service without specifying entity.""" From 370e2ffa5a454dd85f4d478aeb237acb6601528c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 14:57:15 -0800 Subject: [PATCH 107/378] Fix coordinator reference (#31467) --- homeassistant/components/hue/sensor_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 3db07ba2e5b..f57b0f98d30 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -209,7 +209,7 @@ class GenericHueSensor(entity.Entity): Only used by the generic entity update service. """ - await self.bridge.sensor_manager.coordinator.coordinator.async_request_refresh() + await self.bridge.sensor_manager.coordinator.async_request_refresh() @property def device_info(self): From 2c439af1656959ca62f2bd304726513d2023d198 Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 5 Feb 2020 00:26:47 +0100 Subject: [PATCH 108/378] Fix iCloud device battery level can be None (#31468) --- homeassistant/components/icloud/account.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index afa1ad092a2..af7963d8dc1 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -331,14 +331,13 @@ class IcloudDevice: device_status = DEVICE_STATUS_CODES.get(self._status[DEVICE_STATUS], "error") self._attrs[ATTR_DEVICE_STATUS] = device_status - if self._status[DEVICE_BATTERY_STATUS] != "Unknown": - self._battery_level = int(self._status.get(DEVICE_BATTERY_LEVEL, 0) * 100) - self._battery_status = self._status[DEVICE_BATTERY_STATUS] - low_power_mode = self._status[DEVICE_LOW_POWER_MODE] - + self._battery_status = self._status[DEVICE_BATTERY_STATUS] + self._attrs[ATTR_BATTERY_STATUS] = self._battery_status + device_battery_level = self._status.get(DEVICE_BATTERY_LEVEL, 0) + if self._battery_status != "Unknown" and device_battery_level is not None: + self._battery_level = int(device_battery_level * 100) self._attrs[ATTR_BATTERY] = self._battery_level - self._attrs[ATTR_BATTERY_STATUS] = self._battery_status - self._attrs[ATTR_LOW_POWER_MODE] = low_power_mode + self._attrs[ATTR_LOW_POWER_MODE] = self._status[DEVICE_LOW_POWER_MODE] if ( self._status[DEVICE_LOCATION] From e970177eebed9adf444310338d630dda6ca935e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 15:30:15 -0800 Subject: [PATCH 109/378] Use entity.async_request_call in service helper (#31454) * Use entity.async_request_call in service helper * Clean up semaphore handling * Address comments * Simplify call entity service helper * Fix stupid rflink test --- homeassistant/components/rflink/cover.py | 2 + homeassistant/components/rflink/light.py | 2 + homeassistant/components/rflink/switch.py | 2 + homeassistant/helpers/entity.py | 1 - homeassistant/helpers/entity_platform.py | 64 ++++--- homeassistant/helpers/service.py | 115 ++++++------ tests/helpers/test_entity_platform.py | 4 - tests/helpers/test_service.py | 214 +++++++++++----------- 8 files changed, 211 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 5db82a1d4e8..794542cb9d4 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -23,6 +23,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + TYPE_STANDARD = "standard" TYPE_INVERTED = "inverted" diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index db616b92fc4..1ed19569585 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -31,6 +31,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + TYPE_DIMMABLE = "dimmable" TYPE_SWITCHABLE = "switchable" TYPE_HYBRID = "hybrid" diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index 8e0ce9a0c8e..990d76101cc 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -22,6 +22,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a2a0ae840e0..fa649561e3d 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -568,7 +568,6 @@ class Entity(ABC): # call an requests async def async_request_call(self, coro): """Process request batched.""" - if self.parallel_updates: await self.parallel_updates.acquire() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 8fedc198fe2..e71b28f1713 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -62,22 +62,42 @@ class EntityPlatform: # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities if platform is None: - self.parallel_updates = None - self.parallel_updates_semaphore: Optional[asyncio.Semaphore] = None + self.parallel_updates_created = True + self.parallel_updates: Optional[asyncio.Semaphore] = None return - self.parallel_updates = getattr(platform, "PARALLEL_UPDATES", None) - # semaphore will be created on demand - self.parallel_updates_semaphore = None + self.parallel_updates_created = False + self.parallel_updates = None - def _get_parallel_updates_semaphore(self) -> asyncio.Semaphore: - """Get or create a semaphore for parallel updates.""" - if self.parallel_updates_semaphore is None: - self.parallel_updates_semaphore = asyncio.Semaphore( - self.parallel_updates if self.parallel_updates else 1, - loop=self.hass.loop, - ) - return self.parallel_updates_semaphore + @callback + def _get_parallel_updates_semaphore( + self, entity_has_async_update: bool + ) -> Optional[asyncio.Semaphore]: + """Get or create a semaphore for parallel updates. + + Semaphore will be created on demand because we base it off if update method is async or not. + + If parallel updates is set to 0, we skip the semaphore. + If parallel updates is set to a number, we initialize the semaphore to that number. + Default for entities with `async_update` method is 1. Otherwise it's 0. + """ + if self.parallel_updates_created: + return self.parallel_updates + + self.parallel_updates_created = True + + parallel_updates = getattr(self.platform, "PARALLEL_UPDATES", None) + + if parallel_updates is None and not entity_has_async_update: + parallel_updates = 1 + + if parallel_updates == 0: + parallel_updates = None + + if parallel_updates is not None: + self.parallel_updates = asyncio.Semaphore(parallel_updates) + + return self.parallel_updates async def async_setup(self, platform_config, discovery_info=None): """Set up the platform from a config file.""" @@ -282,21 +302,9 @@ class EntityPlatform: entity.hass = self.hass entity.platform = self - - # Async entity - # PARALLEL_UPDATES == None: entity.parallel_updates = None - # PARALLEL_UPDATES == 0: entity.parallel_updates = None - # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) - # Sync entity - # PARALLEL_UPDATES == None: entity.parallel_updates = Semaphore(1) - # PARALLEL_UPDATES == 0: entity.parallel_updates = None - # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) - if hasattr(entity, "async_update") and not self.parallel_updates: - entity.parallel_updates = None - elif not hasattr(entity, "async_update") and self.parallel_updates == 0: - entity.parallel_updates = None - else: - entity.parallel_updates = self._get_parallel_updates_semaphore() + entity.parallel_updates = self._get_parallel_updates_semaphore( + hasattr(entity, "async_update") + ) # Update properties before we generate the entity_id if update_before_add: diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 51f881181af..46ebc467c0b 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -316,16 +316,15 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # Check the permissions - # A list with for each platform in platforms a list of entities to call - # the service on. - platforms_entities = [] + # A list with entities to call the service on. + entity_candidates = [] if entity_perms is None: for platform in platforms: if target_all_entities: - platforms_entities.append(list(platform.entities.values())) + entity_candidates.extend(platform.entities.values()) else: - platforms_entities.append( + entity_candidates.extend( [ entity for entity in platform.entities.values() @@ -337,7 +336,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # If we target all entities, we will select all entities the user # is allowed to control. for platform in platforms: - platforms_entities.append( + entity_candidates.extend( [ entity for entity in platform.entities.values() @@ -362,39 +361,20 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non platform_entities.append(entity) - platforms_entities.append(platform_entities) + entity_candidates.extend(platform_entities) if not target_all_entities: - for platform_entities in platforms_entities: - for entity in platform_entities: - entity_ids.remove(entity.entity_id) + for entity in entity_candidates: + entity_ids.remove(entity.entity_id) if entity_ids: _LOGGER.warning( "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) ) - tasks = [ - _handle_service_platform_call( - hass, func, data, entities, call.context, required_features - ) - for platform, entities in zip(platforms, platforms_entities) - ] + entities = [] - if tasks: - done, pending = await asyncio.wait(tasks) - assert not pending - for future in done: - future.result() # pop exception if have - - -async def _handle_service_platform_call( - hass, func, data, entities, context, required_features -): - """Handle a function call.""" - tasks = [] - - for entity in entities: + for entity in entity_candidates: if not entity.available: continue @@ -404,27 +384,33 @@ async def _handle_service_platform_call( ): continue - entity.async_set_context(context) + entities.append(entity) - if isinstance(func, str): - result = hass.async_add_job(partial(getattr(entity, func), **data)) - else: - result = hass.async_add_job(func, entity, data) + if not entities: + return - # Guard because callback functions do not return a task when passed to async_add_job. - if result is not None: - result = await result - - if asyncio.iscoroutine(result): - _LOGGER.error( - "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", - func, - entity.entity_id, + done, pending = await asyncio.wait( + [ + entity.async_request_call( + _handle_entity_call(hass, entity, func, data, call.context) ) - await result + for entity in entities + ] + ) + assert not pending + for future in done: + future.result() # pop exception if have - if entity.should_poll: - tasks.append(entity.async_update_ha_state(True)) + tasks = [] + + for entity in entities: + if not entity.should_poll: + continue + + # Context expires if the turn on commands took a long time. + # Set context again so it's there when we update + entity.async_set_context(call.context) + tasks.append(entity.async_update_ha_state(True)) if tasks: done, pending = await asyncio.wait(tasks) @@ -433,6 +419,28 @@ async def _handle_service_platform_call( future.result() # pop exception if have +async def _handle_entity_call(hass, entity, func, data, context): + """Handle calling service method.""" + entity.async_set_context(context) + + if isinstance(func, str): + result = hass.async_add_job(partial(getattr(entity, func), **data)) + else: + result = hass.async_add_job(func, entity, data) + + # Guard because callback functions do not return a task when passed to async_add_job. + if result is not None: + await result + + if asyncio.iscoroutine(result): + _LOGGER.error( + "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", + func, + entity.entity_id, + ) + await result + + @bind_hass @ha.callback def async_register_admin_service( @@ -474,6 +482,7 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: return await service_handler(call) user = await hass.auth.async_get_user(call.context.user_id) + if user is None: raise UnknownUser( context=call.context, @@ -482,14 +491,12 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: ) reg = await hass.helpers.entity_registry.async_get_registry() - entities = [ - entity.entity_id - for entity in reg.entities.values() - if entity.platform == domain - ] - for entity_id in entities: - if user.permissions.check_entity(entity_id, POLICY_CONTROL): + for entity in reg.entities.values(): + if entity.platform != domain: + continue + + if user.permissions.check_entity(entity.entity_id, POLICY_CONTROL): return await service_handler(call) raise Unauthorized( diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 74105689957..ee43f5d4f1d 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -270,8 +270,6 @@ async def test_parallel_updates_async_platform_with_constant(hass): handle = list(component._platforms.values())[-1] - assert handle.parallel_updates == 2 - class AsyncEntity(MockEntity): """Mock entity that has async_update.""" @@ -296,7 +294,6 @@ async def test_parallel_updates_sync_platform(hass): await component.async_setup({DOMAIN: {"platform": "platform"}}) handle = list(component._platforms.values())[-1] - assert handle.parallel_updates is None class SyncEntity(MockEntity): """Mock entity that has update.""" @@ -323,7 +320,6 @@ async def test_parallel_updates_sync_platform_with_constant(hass): await component.async_setup({DOMAIN: {"platform": "platform"}}) handle = list(component._platforms.values())[-1] - assert handle.parallel_updates == 2 class SyncEntity(MockEntity): """Mock entity that has update.""" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 106fdfabf2d..cc4098a613a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -39,31 +39,29 @@ from tests.common import ( @pytest.fixture -def mock_service_platform_call(): +def mock_handle_entity_call(): """Mock service platform call.""" with patch( - "homeassistant.helpers.service._handle_service_platform_call", + "homeassistant.helpers.service._handle_entity_call", side_effect=lambda *args: mock_coro(), ) as mock_call: yield mock_call @pytest.fixture -def mock_entities(): +def mock_entities(hass): """Return mock entities in an ordered dict.""" - kitchen = Mock( + kitchen = MockEntity( entity_id="light.kitchen", available=True, should_poll=False, supported_features=1, - platform="test_domain", ) - living_room = Mock( + living_room = MockEntity( entity_id="light.living_room", available=True, should_poll=False, supported_features=0, - platform="test_domain", ) entities = OrderedDict() entities[kitchen.entity_id] = kitchen @@ -374,7 +372,7 @@ async def test_call_context_user_not_exist(hass): assert err.value.context.user_id == "non-existing" -async def test_call_context_target_all(hass, mock_service_platform_call, mock_entities): +async def test_call_context_target_all(hass, mock_handle_entity_call, mock_entities): """Check we only target allowed entities if targeting all.""" with patch( "homeassistant.auth.AuthManager.async_get_user", @@ -398,13 +396,12 @@ async def test_call_context_target_all(hass, mock_service_platform_call, mock_en ), ) - assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][3] - assert entities == [mock_entities["light.kitchen"]] + assert len(mock_handle_entity_call.mock_calls) == 1 + assert mock_handle_entity_call.mock_calls[0][1][1].entity_id == "light.kitchen" async def test_call_context_target_specific( - hass, mock_service_platform_call, mock_entities + hass, mock_handle_entity_call, mock_entities ): """Check targeting specific entities.""" with patch( @@ -429,13 +426,12 @@ async def test_call_context_target_specific( ), ) - assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][3] - assert entities == [mock_entities["light.kitchen"]] + assert len(mock_handle_entity_call.mock_calls) == 1 + assert mock_handle_entity_call.mock_calls[0][1][1].entity_id == "light.kitchen" async def test_call_context_target_specific_no_auth( - hass, mock_service_platform_call, mock_entities + hass, mock_handle_entity_call, mock_entities ): """Check targeting specific entities without auth.""" with pytest.raises(exceptions.Unauthorized) as err: @@ -459,9 +455,7 @@ async def test_call_context_target_specific_no_auth( assert err.value.entity_id == "light.kitchen" -async def test_call_no_context_target_all( - hass, mock_service_platform_call, mock_entities -): +async def test_call_no_context_target_all(hass, mock_handle_entity_call, mock_entities): """Check we target all if no user context given.""" await service.entity_service_call( hass, @@ -472,13 +466,14 @@ async def test_call_no_context_target_all( ), ) - assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][3] - assert entities == list(mock_entities.values()) + assert len(mock_handle_entity_call.mock_calls) == 2 + assert [call[1][1] for call in mock_handle_entity_call.mock_calls] == list( + mock_entities.values() + ) async def test_call_no_context_target_specific( - hass, mock_service_platform_call, mock_entities + hass, mock_handle_entity_call, mock_entities ): """Check we can target specified entities.""" await service.entity_service_call( @@ -492,13 +487,12 @@ async def test_call_no_context_target_specific( ), ) - assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][3] - assert entities == [mock_entities["light.kitchen"]] + assert len(mock_handle_entity_call.mock_calls) == 1 + assert mock_handle_entity_call.mock_calls[0][1][1].entity_id == "light.kitchen" async def test_call_with_match_all( - hass, mock_service_platform_call, mock_entities, caplog + hass, mock_handle_entity_call, mock_entities, caplog ): """Check we only target allowed entities if targeting all.""" await service.entity_service_call( @@ -508,20 +502,13 @@ async def test_call_with_match_all( ha.ServiceCall("test_domain", "test_service", {"entity_id": "all"}), ) - assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][3] - assert entities == [ - mock_entities["light.kitchen"], - mock_entities["light.living_room"], - ] - assert ( - "Not passing an entity ID to a service to target all entities is deprecated" - ) not in caplog.text + assert len(mock_handle_entity_call.mock_calls) == 2 + assert [call[1][1] for call in mock_handle_entity_call.mock_calls] == list( + mock_entities.values() + ) -async def test_call_with_omit_entity_id( - hass, mock_service_platform_call, mock_entities -): +async def test_call_with_omit_entity_id(hass, mock_handle_entity_call, mock_entities): """Check service call if we do not pass an entity ID.""" await service.entity_service_call( hass, @@ -530,9 +517,7 @@ async def test_call_with_omit_entity_id( ha.ServiceCall("test_domain", "test_service"), ) - assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][3] - assert entities == [] + assert len(mock_handle_entity_call.mock_calls) == 0 async def test_register_admin_service(hass, hass_read_only_user, hass_admin_user): @@ -644,96 +629,113 @@ async def test_domain_control_unknown(hass, mock_entities): assert len(calls) == 0 -async def test_domain_control_unauthorized(hass, hass_read_only_user, mock_entities): +async def test_domain_control_unauthorized(hass, hass_read_only_user): """Test domain verification in a service call with an unauthorized user.""" - calls = [] - - async def mock_service_log(call): - """Define a protected service.""" - calls.append(call) - - with patch( - "homeassistant.helpers.entity_registry.async_get_registry", - return_value=mock_coro(Mock(entities=mock_entities)), - ): - protected_mock_service = hass.helpers.service.verify_domain_control( - "test_domain" - )(mock_service_log) - - hass.services.async_register( - "test_domain", "test_service", protected_mock_service, schema=None - ) - - with pytest.raises(exceptions.Unauthorized): - await hass.services.async_call( - "test_domain", - "test_service", - {}, - blocking=True, - context=ha.Context(user_id=hass_read_only_user.id), + mock_registry( + hass, + { + "light.kitchen": ent_reg.RegistryEntry( + entity_id="light.kitchen", unique_id="kitchen", platform="test_domain", ) + }, + ) + + calls = [] + + async def mock_service_log(call): + """Define a protected service.""" + calls.append(call) + + protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")( + mock_service_log + ) + + hass.services.async_register( + "test_domain", "test_service", protected_mock_service, schema=None + ) + + with pytest.raises(exceptions.Unauthorized): + await hass.services.async_call( + "test_domain", + "test_service", + {}, + blocking=True, + context=ha.Context(user_id=hass_read_only_user.id), + ) + + assert len(calls) == 0 -async def test_domain_control_admin(hass, hass_admin_user, mock_entities): +async def test_domain_control_admin(hass, hass_admin_user): """Test domain verification in a service call with an admin user.""" + mock_registry( + hass, + { + "light.kitchen": ent_reg.RegistryEntry( + entity_id="light.kitchen", unique_id="kitchen", platform="test_domain", + ) + }, + ) + calls = [] async def mock_service_log(call): """Define a protected service.""" calls.append(call) - with patch( - "homeassistant.helpers.entity_registry.async_get_registry", - return_value=mock_coro(Mock(entities=mock_entities)), - ): - protected_mock_service = hass.helpers.service.verify_domain_control( - "test_domain" - )(mock_service_log) + protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")( + mock_service_log + ) - hass.services.async_register( - "test_domain", "test_service", protected_mock_service, schema=None - ) + hass.services.async_register( + "test_domain", "test_service", protected_mock_service, schema=None + ) - await hass.services.async_call( - "test_domain", - "test_service", - {}, - blocking=True, - context=ha.Context(user_id=hass_admin_user.id), - ) + await hass.services.async_call( + "test_domain", + "test_service", + {}, + blocking=True, + context=ha.Context(user_id=hass_admin_user.id), + ) - assert len(calls) == 1 + assert len(calls) == 1 -async def test_domain_control_no_user(hass, mock_entities): +async def test_domain_control_no_user(hass): """Test domain verification in a service call with no user.""" + mock_registry( + hass, + { + "light.kitchen": ent_reg.RegistryEntry( + entity_id="light.kitchen", unique_id="kitchen", platform="test_domain", + ) + }, + ) + calls = [] async def mock_service_log(call): """Define a protected service.""" calls.append(call) - with patch( - "homeassistant.helpers.entity_registry.async_get_registry", - return_value=mock_coro(Mock(entities=mock_entities)), - ): - protected_mock_service = hass.helpers.service.verify_domain_control( - "test_domain" - )(mock_service_log) + protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")( + mock_service_log + ) - hass.services.async_register( - "test_domain", "test_service", protected_mock_service, schema=None - ) + hass.services.async_register( + "test_domain", "test_service", protected_mock_service, schema=None + ) - await hass.services.async_call( - "test_domain", - "test_service", - {}, - blocking=True, - context=ha.Context(user_id=None), - ) + await hass.services.async_call( + "test_domain", + "test_service", + {}, + blocking=True, + context=ha.Context(user_id=None), + ) - assert len(calls) == 1 + assert len(calls) == 1 async def test_extract_from_service_available_device(hass): From c85a7934ed5cc28282be678b511af1cd93a34727 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 16:13:29 -0800 Subject: [PATCH 110/378] Add brightness_step to light.turn_on (#31452) * Clean up light turn on service * Add brightness_step to turn_on schema * Fix import * Fix imports 2 * Fix RFLink test --- homeassistant/components/light/__init__.py | 111 +++++++++--------- homeassistant/components/light/intent.py | 2 +- homeassistant/components/light/services.yaml | 6 + homeassistant/components/xiaomi_miio/light.py | 3 +- tests/components/light/test_init.py | 34 ++++++ tests/components/rflink/test_light.py | 18 ++- .../custom_components/test/light.py | 14 ++- 7 files changed, 115 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 791f7328cf8..5b9b923cc56 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,5 +1,4 @@ """Provides functionality to interact with lights.""" -import asyncio import csv from datetime import timedelta import logging @@ -8,15 +7,12 @@ from typing import Dict, Optional, Tuple import voluptuous as vol -from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.exceptions import Unauthorized, UnknownUser import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -61,6 +57,8 @@ ATTR_WHITE_VALUE = "white_value" # Brightness of the light, 0..255 or percentage ATTR_BRIGHTNESS = "brightness" ATTR_BRIGHTNESS_PCT = "brightness_pct" +ATTR_BRIGHTNESS_STEP = "brightness_step" +ATTR_BRIGHTNESS_STEP_PCT = "brightness_step_pct" # String representing a profile (built-in ones or external defined). ATTR_PROFILE = "profile" @@ -87,12 +85,16 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) +VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255)) +VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100)) LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP, + vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) @@ -169,7 +171,7 @@ def preprocess_turn_off(params): """Process data for turning light off if brightness is 0.""" if ATTR_BRIGHTNESS in params and params[ATTR_BRIGHTNESS] == 0: # Zero brightness: Light will be turned off - params = {k: v for k, v in params.items() if k in [ATTR_TRANSITION, ATTR_FLASH]} + params = {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} return (True, params) # Light should be turned off return (False, None) # Light should be turned on @@ -187,70 +189,65 @@ async def async_setup(hass, config): if not profiles_valid: return False - async def async_handle_light_on_service(service): - """Handle a turn light on service call.""" - # Get the validated data - params = service.data.copy() + def preprocess_data(data): + """Preprocess the service data.""" + base = {} - # Convert the entity ids to valid light ids - target_lights = await component.async_extract_from_service(service) - params.pop(ATTR_ENTITY_ID, None) + for entity_field in cv.ENTITY_SERVICE_FIELDS: + if entity_field in data: + base[entity_field] = data.pop(entity_field) - if service.context.user_id: - user = await hass.auth.async_get_user(service.context.user_id) - if user is None: - raise UnknownUser(context=service.context) + preprocess_turn_on_alternatives(data) + turn_lights_off, off_params = preprocess_turn_off(data) - entity_perms = user.permissions.check_entity + base["params"] = data + base["turn_lights_off"] = turn_lights_off + base["off_params"] = off_params - for light in target_lights: - if not entity_perms(light, POLICY_CONTROL): - raise Unauthorized( - context=service.context, - entity_id=light, - permission=POLICY_CONTROL, - ) + return base - preprocess_turn_on_alternatives(params) - turn_lights_off, off_params = preprocess_turn_off(params) + async def async_handle_light_on_service(light, call): + """Handle turning a light on. - poll_lights = [] - change_tasks = [] - for light in target_lights: - light.async_set_context(service.context) + If brightness is set to 0, this service will turn the light off. + """ + params = call.data["params"] + turn_light_off = call.data["turn_lights_off"] + off_params = call.data["off_params"] + + if not params: + default_profile = Profiles.get_default(light.entity_id) + + if default_profile is not None: + params = {ATTR_PROFILE: default_profile} + preprocess_turn_on_alternatives(params) + turn_light_off, off_params = preprocess_turn_off(params) + + elif ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params: + brightness = light.brightness if light.is_on else 0 + + params = params.copy() + + if ATTR_BRIGHTNESS_STEP in params: + brightness += params.pop(ATTR_BRIGHTNESS_STEP) - pars = params - off_pars = off_params - turn_light_off = turn_lights_off - if not pars: - pars = params.copy() - pars[ATTR_PROFILE] = Profiles.get_default(light.entity_id) - preprocess_turn_on_alternatives(pars) - turn_light_off, off_pars = preprocess_turn_off(pars) - if turn_light_off: - task = light.async_request_call(light.async_turn_off(**off_pars)) else: - task = light.async_request_call(light.async_turn_on(**pars)) + brightness += int(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255) - change_tasks.append(task) + params[ATTR_BRIGHTNESS] = max(0, min(255, brightness)) + turn_light_off, off_params = preprocess_turn_off(params) - if light.should_poll: - poll_lights.append(light) - - if change_tasks: - await asyncio.wait(change_tasks) - - if poll_lights: - await asyncio.wait( - [light.async_update_ha_state(True) for light in poll_lights] - ) + if turn_light_off: + await light.async_turn_off(**off_params) + else: + await light.async_turn_on(**params) # Listen for light on and light off service calls. - hass.services.async_register( - DOMAIN, + + component.async_register_entity_service( SERVICE_TURN_ON, + vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data), async_handle_light_on_service, - schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), ) component.async_register_entity_service( diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index ea8899c44fc..c172ac1330a 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -1,6 +1,7 @@ """Intents for the light integration.""" import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv @@ -8,7 +9,6 @@ import homeassistant.util.color as color_util from . import ( ATTR_BRIGHTNESS_PCT, - ATTR_ENTITY_ID, ATTR_RGB_COLOR, DOMAIN, SERVICE_TURN_ON, diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 449e5ea5aaf..a2b71f5632b 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -36,6 +36,12 @@ turn_on: brightness_pct: description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. example: 47 + brightness_step: + description: Change brightness by an amount. Should be between -255..255. + example: -25.5 + brightness_step_pct: + description: Change brightness by a percentage. Should be between -100..100. + example: -10 profile: description: Name of a light profile to use. example: relax diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index bcc83bae454..61462bcdbc0 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -19,7 +19,6 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_ENTITY_ID, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -27,7 +26,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, Light, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 676fa4ec849..49bc626a957 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -462,3 +462,37 @@ async def test_light_turn_on_auth(hass, hass_admin_user): True, core.Context(user_id=hass_admin_user.id), ) + + +async def test_light_brightness_step(hass): + """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() + entity = platform.ENTITIES[0] + entity.supported_features = light.SUPPORT_BRIGHTNESS + entity.brightness = 100 + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.attributes["brightness"] == 100 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity.entity_id, "brightness_step": -10}, + True, + ) + + _, data = entity.last_call("turn_on") + assert data["brightness"] == 90, data + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity.entity_id, "brightness_step_pct": 10}, + True, + ) + + _, data = entity.last_call("turn_on") + assert data["brightness"] == 125, data diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index 970c532f22e..5dc06b5b2ff 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -298,18 +298,16 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch): DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"} ) - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DOMAIN + ".test"} - ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DOMAIN + ".test"}, blocking=True ) - await hass.async_block_till_done() - - assert protocol.send_command_ack.call_args_list[0][0][1] == "off" - assert protocol.send_command_ack.call_args_list[1][0][1] == "on" - assert protocol.send_command_ack.call_args_list[2][0][1] == "on" - assert protocol.send_command_ack.call_args_list[3][0][1] == "on" + assert [call[0][1] for call in protocol.send_command_ack.call_args_list] == [ + "off", + "on", + "on", + "on", + ] async def test_type_toggle(hass, monkeypatch): diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 4b018adb5cb..d3f96c367d8 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -3,6 +3,7 @@ Provide a mock light platform. Call init before using it in your tests to ensure clean test data. """ +from homeassistant.components.light import Light from homeassistant.const import STATE_OFF, STATE_ON from tests.common import MockToggleEntity @@ -18,9 +19,9 @@ def init(empty=False): [] if empty else [ - MockToggleEntity("Ceiling", STATE_ON), - MockToggleEntity("Ceiling", STATE_OFF), - MockToggleEntity(None, STATE_OFF), + MockLight("Ceiling", STATE_ON), + MockLight("Ceiling", STATE_OFF), + MockLight(None, STATE_OFF), ] ) @@ -30,3 +31,10 @@ async def async_setup_platform( ): """Return mock entities.""" async_add_entities_callback(ENTITIES) + + +class MockLight(MockToggleEntity, Light): + """Mock light class.""" + + brightness = None + supported_features = 0 From 3801d5adf4521d120cc36e83a6ced2b41a5cdba3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 5 Feb 2020 00:31:54 +0000 Subject: [PATCH 111/378] [ci skip] Translation update --- .../garmin_connect/.translations/pl.json | 2 +- .../components/linky/.translations/bg.json | 4 -- .../components/linky/.translations/ca.json | 4 +- .../components/linky/.translations/da.json | 4 +- .../components/linky/.translations/de.json | 4 +- .../components/linky/.translations/en.json | 4 +- .../linky/.translations/es-419.json | 4 -- .../components/linky/.translations/es.json | 4 +- .../components/linky/.translations/fr.json | 3 +- .../components/linky/.translations/it.json | 4 +- .../components/linky/.translations/ko.json | 4 +- .../components/linky/.translations/lb.json | 4 +- .../components/linky/.translations/nl.json | 4 -- .../components/linky/.translations/no.json | 4 +- .../components/linky/.translations/pl.json | 4 +- .../components/linky/.translations/pt-BR.json | 1 - .../components/linky/.translations/pt.json | 6 --- .../components/linky/.translations/ru.json | 4 +- .../components/linky/.translations/sl.json | 4 -- .../linky/.translations/zh-Hans.json | 3 -- .../linky/.translations/zh-Hant.json | 4 +- .../meteo_france/.translations/de.json | 17 +++++++++ .../components/mikrotik/.translations/pl.json | 37 +++++++++++++++++++ .../samsungtv/.translations/da.json | 2 + .../samsungtv/.translations/ko.json | 2 + .../samsungtv/.translations/no.json | 8 ++-- .../samsungtv/.translations/pl.json | 2 + .../samsungtv/.translations/ru.json | 8 ++-- .../samsungtv/.translations/zh-Hant.json | 4 +- .../components/vizio/.translations/en.json | 1 + 30 files changed, 88 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/meteo_france/.translations/de.json create mode 100644 homeassistant/components/mikrotik/.translations/pl.json diff --git a/homeassistant/components/garmin_connect/.translations/pl.json b/homeassistant/components/garmin_connect/.translations/pl.json index 45d0296b668..df80f3466e9 100644 --- a/homeassistant/components/garmin_connect/.translations/pl.json +++ b/homeassistant/components/garmin_connect/.translations/pl.json @@ -19,6 +19,6 @@ "title": "Garmin Connect" } }, - "title": "Garmin Connect" + "title": "Gar min Connect" } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/bg.json b/homeassistant/components/linky/.translations/bg.json index 6eeb898ee1f..cc5239eaf3c 100644 --- a/homeassistant/components/linky/.translations/bg.json +++ b/homeassistant/components/linky/.translations/bg.json @@ -1,13 +1,9 @@ { "config": { - "abort": { - "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" - }, "error": { "access": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e Enedis.fr, \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0438", "enedis": "Enedis.fr \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", - "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b", "wrong_login": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435\u0439\u043b\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438" }, "step": { diff --git a/homeassistant/components/linky/.translations/ca.json b/homeassistant/components/linky/.translations/ca.json index ff242c556fd..9c4a3a19067 100644 --- a/homeassistant/components/linky/.translations/ca.json +++ b/homeassistant/components/linky/.translations/ca.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", - "username_exists": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat" }, "error": { "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet", "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)", "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)", - "username_exists": "El compte ja ha estat configurat", "wrong_login": "Error d\u2019inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" }, "step": { diff --git a/homeassistant/components/linky/.translations/da.json b/homeassistant/components/linky/.translations/da.json index a0bcc5f9b61..2097d094a3a 100644 --- a/homeassistant/components/linky/.translations/da.json +++ b/homeassistant/components/linky/.translations/da.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigureret", - "username_exists": "Kontoen er allerede konfigureret" + "already_configured": "Kontoen er allerede konfigureret" }, "error": { "access": "Kunne ikke f\u00e5 adgang til Enedis.fr, kontroller din internetforbindelse", "enedis": "Enedis.fr svarede med en fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", "unknown": "Ukendt fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", - "username_exists": "Kontoen er allerede konfigureret", "wrong_login": "Loginfejl: Kontroller din e-mail og adgangskode" }, "step": { diff --git a/homeassistant/components/linky/.translations/de.json b/homeassistant/components/linky/.translations/de.json index cf782edfdc4..83e56a52c6c 100644 --- a/homeassistant/components/linky/.translations/de.json +++ b/homeassistant/components/linky/.translations/de.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto bereits konfiguriert", - "username_exists": "Konto bereits konfiguriert" + "already_configured": "Konto bereits konfiguriert" }, "error": { "access": "Konnte nicht auf Enedis.fr zugreifen, \u00fcberpr\u00fcfe bitte die Internetverbindung", "enedis": "Enedis.fr antwortete mit einem Fehler: wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", "unknown": "Unbekannter Fehler: Wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", - "username_exists": "Konto bereits konfiguriert", "wrong_login": "Login-Fehler: Pr\u00fcfe bitte E-Mail & Passwort" }, "step": { diff --git a/homeassistant/components/linky/.translations/en.json b/homeassistant/components/linky/.translations/en.json index 95964cb7805..13d2553b0c7 100644 --- a/homeassistant/components/linky/.translations/en.json +++ b/homeassistant/components/linky/.translations/en.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Account already configured", - "username_exists": "Account already configured" + "already_configured": "Account already configured" }, "error": { "access": "Could not access to Enedis.fr, please check your internet connection", "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)", - "username_exists": "Account already configured", "wrong_login": "Login error: please check your email & password" }, "step": { diff --git a/homeassistant/components/linky/.translations/es-419.json b/homeassistant/components/linky/.translations/es-419.json index 130a856826e..5bddb534146 100644 --- a/homeassistant/components/linky/.translations/es-419.json +++ b/homeassistant/components/linky/.translations/es-419.json @@ -1,13 +1,9 @@ { "config": { - "abort": { - "username_exists": "La cuenta ya ha sido configurada" - }, "error": { "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet.", "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", - "username_exists": "La cuenta ya ha sido configurada", "wrong_login": "Error de inicio de sesi\u00f3n: por favor revise su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" }, "step": { diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json index 334c5eaa0f0..c0052c356b2 100644 --- a/homeassistant/components/linky/.translations/es.json +++ b/homeassistant/components/linky/.translations/es.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada", - "username_exists": "Cuenta ya configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet", "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11:00 y las 2 de la ma\u00f1ana)", "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 23:00 y las 02:00 horas).", - "username_exists": "Cuenta ya configurada", "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" }, "step": { diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json index 6ff99c41a16..6dba7e9af89 100644 --- a/homeassistant/components/linky/.translations/fr.json +++ b/homeassistant/components/linky/.translations/fr.json @@ -1,13 +1,12 @@ { "config": { "abort": { - "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" }, "error": { "access": "Impossible d'acc\u00e9der \u00e0 Enedis.fr, merci de v\u00e9rifier votre connexion internet", "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", - "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", "wrong_login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe" }, "step": { diff --git a/homeassistant/components/linky/.translations/it.json b/homeassistant/components/linky/.translations/it.json index 15f15bf8b8e..67418f616ad 100644 --- a/homeassistant/components/linky/.translations/it.json +++ b/homeassistant/components/linky/.translations/it.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Account gi\u00e0 configurato", - "username_exists": "Account gi\u00e0 configurato" + "already_configured": "Account gi\u00e0 configurato" }, "error": { "access": "Impossibile accedere a Enedis.fr, si prega di controllare la connessione internet", "enedis": "Enedis.fr ha risposto con un errore: si prega di riprovare pi\u00f9 tardi (di solito non tra le 23:00 e le 02:00).", "unknown": "Errore sconosciuto: riprova pi\u00f9 tardi (in genere non tra le 23:00 e le 02:00)", - "username_exists": "Account gi\u00e0 configurato", "wrong_login": "Errore di accesso: si prega di controllare la tua E-mail e la password" }, "step": { diff --git a/homeassistant/components/linky/.translations/ko.json b/homeassistant/components/linky/.translations/ko.json index beac46255db..78c825398d6 100644 --- a/homeassistant/components/linky/.translations/ko.json +++ b/homeassistant/components/linky/.translations/ko.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "access": "Enedis.fr \uc5d0 \uc811\uc18d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137 \uc5f0\uacb0\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694", "enedis": "Enedis.fr \uc774 \uc624\ub958\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", - "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "wrong_login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" }, "step": { diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json index 8279b8a7d6f..b4c10bec367 100644 --- a/homeassistant/components/linky/.translations/lb.json +++ b/homeassistant/components/linky/.translations/lb.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Kont ass scho konfigur\u00e9iert", - "username_exists": "Kont ass scho konfigur\u00e9iert" + "already_configured": "Kont ass scho konfigur\u00e9iert" }, "error": { "access": "Keng Verbindung zu Enedis.fr, iwwerpr\u00e9ift d'Internet Verbindung", "enedis": "Enedis.fr huet mat engem Feeler ge\u00e4ntwert: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", - "username_exists": "Kont ass scho konfigur\u00e9iert", "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" }, "step": { diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json index 89759fdf216..ecc566c8b87 100644 --- a/homeassistant/components/linky/.translations/nl.json +++ b/homeassistant/components/linky/.translations/nl.json @@ -1,13 +1,9 @@ { "config": { - "abort": { - "username_exists": "Account reeds geconfigureerd" - }, "error": { "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding", "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", "unknown": "Onbekende fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", - "username_exists": "Account reeds geconfigureerd", "wrong_login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord" }, "step": { diff --git a/homeassistant/components/linky/.translations/no.json b/homeassistant/components/linky/.translations/no.json index 9951a5c97b4..77b3bac8032 100644 --- a/homeassistant/components/linky/.translations/no.json +++ b/homeassistant/components/linky/.translations/no.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert", - "username_exists": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert" }, "error": { "access": "Kunne ikke f\u00e5 tilgang til Enedis.fr, vennligst sjekk internettforbindelsen din", "enedis": "Enedis.fr svarte med en feil: vennligst pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", - "username_exists": "Kontoen er allerede konfigurert", "wrong_login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt" }, "step": { diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index 7ab291ceff4..51f96dcf17a 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane", - "username_exists": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe", "enedis": "Enedis.fr odpowiedzia\u0142 b\u0142\u0119dem: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy 23:00, a 2:00)", "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy godzin\u0105 23:00, a 2:00)", - "username_exists": "Konto jest ju\u017c skonfigurowane", "wrong_login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o" }, "step": { diff --git a/homeassistant/components/linky/.translations/pt-BR.json b/homeassistant/components/linky/.translations/pt-BR.json index 23f519353b4..9a4a710e522 100644 --- a/homeassistant/components/linky/.translations/pt-BR.json +++ b/homeassistant/components/linky/.translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "error": { - "username_exists": "Conta j\u00e1 configurada", "wrong_login": "Erro de Login: por favor, verifique seu e-mail e senha" }, "step": { diff --git a/homeassistant/components/linky/.translations/pt.json b/homeassistant/components/linky/.translations/pt.json index 67e742c5813..54619af958e 100644 --- a/homeassistant/components/linky/.translations/pt.json +++ b/homeassistant/components/linky/.translations/pt.json @@ -1,11 +1,5 @@ { "config": { - "abort": { - "username_exists": "Conta j\u00e1 configurada" - }, - "error": { - "username_exists": "Conta j\u00e1 configurada" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 5f952a29e78..a868f9666c5 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", - "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", - "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json index 9e9d6668fcb..ab5e054db1e 100644 --- a/homeassistant/components/linky/.translations/sl.json +++ b/homeassistant/components/linky/.translations/sl.json @@ -1,13 +1,9 @@ { "config": { - "abort": { - "username_exists": "Ra\u010dun \u017ee nastavljen" - }, "error": { "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo", "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)", "unknown": "Neznana napaka: Prosimo, poskusite pozneje (obi\u010dajno ne med 23. in 2. uro)", - "username_exists": "Ra\u010dun \u017ee nastavljen", "wrong_login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo" }, "step": { diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json index b450a3cbdb0..2c6b3ba34b5 100644 --- a/homeassistant/components/linky/.translations/zh-Hans.json +++ b/homeassistant/components/linky/.translations/zh-Hans.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "username_exists": "\u8d26\u6237\u5df2\u914d\u7f6e\u5b8c\u6210" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/linky/.translations/zh-Hant.json b/homeassistant/components/linky/.translations/zh-Hant.json index 51622e2f6f7..92ad3ef0ca1 100644 --- a/homeassistant/components/linky/.translations/zh-Hant.json +++ b/homeassistant/components/linky/.translations/zh-Hant.json @@ -1,14 +1,12 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "access": "\u7121\u6cd5\u8a2a\u554f Enedis.fr\uff0c\u8acb\u6aa2\u67e5\u60a8\u7684\u7db2\u969b\u7db2\u8def\u9023\u7dda", "enedis": "Endis.fr \u56de\u5831\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", - "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "wrong_login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027" }, "step": { diff --git a/homeassistant/components/meteo_france/.translations/de.json b/homeassistant/components/meteo_france/.translations/de.json new file mode 100644 index 00000000000..0e99c1de0ce --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Stadt bereits konfiguriert", + "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut" + }, + "step": { + "user": { + "data": { + "city": "Stadt" + }, + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/pl.json b/homeassistant/components/mikrotik/.translations/pl.json new file mode 100644 index 00000000000..0f0a587a573 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/pl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikronik jest ju\u017c skonfigurowany" + }, + "error": { + "cannot_connect": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", + "name_exists": "Nazwa istnieje", + "wrong_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "U\u017cyj ssl" + }, + "title": "Skonfiguruj router Mikrotik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "W\u0142\u0105cz ping ARP", + "detection_time": "Rozwa\u017c interwa\u0142 domowy", + "force_dhcp": "Wymu\u015b skanowanie przy u\u017cyciu DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json index 594127688c2..117069eb016 100644 --- a/homeassistant/components/samsungtv/.translations/da.json +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -5,8 +5,10 @@ "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.", "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", + "not_successful": "Kan ikke oprette forbindelse til denne Samsung tv-enhed.", "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." }, + "flow_title": "Samsung-tv: {model}", "step": { "confirm": { "description": "Vil du konfigurere Samsung-tv {model}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", diff --git a/homeassistant/components/samsungtv/.translations/ko.json b/homeassistant/components/samsungtv/.translations/ko.json index 2817c36989b..f7656eb9035 100644 --- a/homeassistant/components/samsungtv/.translations/ko.json +++ b/homeassistant/components/samsungtv/.translations/ko.json @@ -5,8 +5,10 @@ "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "not_found": "\uc9c0\uc6d0\ub418\ub294 \uc0bc\uc131 TV \ubaa8\ub378\uc774 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "not_successful": "\uc0bc\uc131 TV \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, + "flow_title": "\uc0bc\uc131 TV: {model}", "step": { "confirm": { "description": "\uc0bc\uc131 TV {model} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV \uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc744 \ub36e\uc5b4\uc501\ub2c8\ub2e4.", diff --git a/homeassistant/components/samsungtv/.translations/no.json b/homeassistant/components/samsungtv/.translations/no.json index dcd437642b2..544ab581be8 100644 --- a/homeassistant/components/samsungtv/.translations/no.json +++ b/homeassistant/components/samsungtv/.translations/no.json @@ -3,13 +3,15 @@ "abort": { "already_configured": "Denne Samsung TV-en er allerede konfigurert.", "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.", - "auth_missing": "Home Assistant er ikke autentisert for \u00e5 koble til denne Samsung TV-en.", + "auth_missing": "Home Assistant er ikke autorisert til \u00e5 koble til denne Samsung-TV. Vennligst kontroller innstillingene for TV-en for \u00e5 autorisere Home Assistent.", "not_found": "Ingen st\u00f8ttede Samsung TV-enheter funnet i nettverket.", + "not_successful": "Kan ikke koble til denne Samsung TV-enheten.", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", + "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", "title": "Samsung TV" }, "user": { @@ -17,7 +19,7 @@ "host": "Vert eller IP-adresse", "name": "Navn" }, - "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", + "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/samsungtv/.translations/pl.json b/homeassistant/components/samsungtv/.translations/pl.json index e31aea01d46..a758c2ad379 100644 --- a/homeassistant/components/samsungtv/.translations/pl.json +++ b/homeassistant/components/samsungtv/.translations/pl.json @@ -5,8 +5,10 @@ "already_in_progress": "Konfiguracja telewizora Samsung jest ju\u017c w toku.", "auth_missing": "Home Assistant nie jest uwierzytelniony, aby po\u0142\u0105czy\u0107 si\u0119 z tym telewizorem Samsung.", "not_found": "W sieci nie znaleziono obs\u0142ugiwanych telewizor\u00f3w Samsung.", + "not_successful": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z tym urz\u0105dzeniem Samsung TV.", "not_supported": "Te telewizor Samsung nie jest obecnie obs\u0142ugiwany." }, + "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'em na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", diff --git a/homeassistant/components/samsungtv/.translations/ru.json b/homeassistant/components/samsungtv/.translations/ru.json index d5dd11a1b80..14f772c5e1d 100644 --- a/homeassistant/components/samsungtv/.translations/ru.json +++ b/homeassistant/components/samsungtv/.translations/ru.json @@ -3,13 +3,15 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", - "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "not_successful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" }, "user": { @@ -17,7 +19,7 @@ "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" } }, diff --git a/homeassistant/components/samsungtv/.translations/zh-Hant.json b/homeassistant/components/samsungtv/.translations/zh-Hant.json index 272dffaa482..80cfa32a6bf 100644 --- a/homeassistant/components/samsungtv/.translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/.translations/zh-Hant.json @@ -3,10 +3,12 @@ "abort": { "already_configured": "\u4e09\u661f\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u4e09\u661f\u96fb\u8996\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002", + "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", "not_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u652f\u63f4\u7684\u4e09\u661f\u96fb\u8996\u3002", + "not_successful": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e09\u661f\u96fb\u8996\u8a2d\u5099\u3002", "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002" }, + "flow_title": "\u4e09\u661f\u96fb\u8996\uff1a{model}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4e09\u661f\u96fb\u8996 {model}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002\u624b\u52d5\u8a2d\u5b9a\u5c07\u6703\u8986\u84cb\u539f\u8a2d\u5b9a\u3002", diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index 60fd9049bb3..80d8f500615 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", "host_exists": "Vizio component with host already configured.", "name_exists": "Vizio component with name already configured.", + "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly.", "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.", "updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly." }, From fce96975917e4189ceb1dc06dbfcfa025831b8d6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 5 Feb 2020 01:37:01 +0100 Subject: [PATCH 112/378] deCONZ - Revert from using disabled_by when setting options (#31446) * Revert from using disabled_by when setting options * Remove signalling for changed options * Evaluate allow group option earlier when adding a group --- .../components/deconz/binary_sensor.py | 17 +++--- homeassistant/components/deconz/climate.py | 9 ++- .../components/deconz/deconz_device.py | 11 ---- homeassistant/components/deconz/gateway.py | 58 +------------------ homeassistant/components/deconz/light.py | 11 ++-- homeassistant/components/deconz/sensor.py | 12 ++-- 6 files changed, 30 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 667eb6db075..6a528a66ba6 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import DeconzEntityHandler, get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" @@ -23,8 +23,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) - entity_handler = DeconzEntityHandler(gateway) - @callback def async_add_sensor(sensors, new=True): """Add binary sensor from deCONZ.""" @@ -32,10 +30,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if new and sensor.BINARY: - new_sensor = DeconzBinarySensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + if ( + new + and sensor.BINARY + and ( + gateway.option_allow_clip_sensor + or not sensor.type.startswith("CLIP") + ) + ): + entities.append(DeconzBinarySensor(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index ba1f1ce846a..7b0f44807ec 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -37,7 +37,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if new and sensor.type in Thermostat.ZHATYPE: + if ( + new + and sensor.type in Thermostat.ZHATYPE + and ( + gateway.option_allow_clip_sensor + or not sensor.type.startswith("CLIP") + ) + ): entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 85fd560da1c..615fd3db473 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -63,17 +63,6 @@ class DeconzDevice(DeconzBase, Entity): Daylight is a virtual sensor from deCONZ that should never be enabled by default. """ - if not self.gateway.option_allow_clip_sensor and self._device.type.startswith( - "CLIP" - ): - return False - - if ( - not self.gateway.option_allow_deconz_groups - and self._device.type == "LightGroup" - ): - return False - if self._device.type == "Daylight": return False diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index f33e753e600..48fecc1ec4f 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,14 +9,7 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity_registry import ( - DISABLED_CONFIG_ENTRY, - async_get_registry, -) +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ALLOW_CLIP_SENSOR, @@ -116,7 +109,6 @@ class DeconzGateway: self.api.start() self.config_entry.add_update_listener(self.async_new_address) - self.config_entry.add_update_listener(self.async_options_updated) return True @@ -144,19 +136,6 @@ class DeconzGateway: self.available = available async_dispatcher_send(self.hass, self.signal_reachable, True) - @property - def signal_options_update(self) -> str: - """Event specific per deCONZ entry to signal new options.""" - return f"deconz-options-{self.bridgeid}" - - @staticmethod - async def async_options_updated(hass, entry) -> None: - """Triggered by config entry options updates.""" - gateway = get_gateway_from_config_entry(hass, entry) - - registry = await async_get_registry(hass) - async_dispatcher_send(hass, gateway.signal_options_update, registry) - @callback def async_signal_new_device(self, device_type) -> str: """Gateway specific event to signal new device.""" @@ -227,38 +206,3 @@ async def get_gateway( except (asyncio.TimeoutError, errors.RequestError): LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) raise CannotConnect - - -class DeconzEntityHandler: - """Platform entity handler to help with updating disabled by.""" - - def __init__(self, gateway) -> None: - """Create an entity handler.""" - self.gateway = gateway - self._entities = [] - - gateway.listeners.append( - async_dispatcher_connect( - gateway.hass, gateway.signal_options_update, self.update_entity_registry - ) - ) - - @callback - def add_entity(self, entity) -> None: - """Add a new entity to handler.""" - self._entities.append(entity) - - @callback - def update_entity_registry(self, entity_registry) -> None: - """Update entity registry disabled by status.""" - for entity in self._entities: - - if entity.entity_registry_enabled_default != entity.enabled: - disabled_by = None - - if entity.enabled: - disabled_by = DISABLED_CONFIG_ENTRY - - entity_registry.async_update_entity( - entity.registry_entry.entity_id, disabled_by=disabled_by - ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index d65f9fb3ee7..e836f1e4490 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -30,7 +30,7 @@ from .const import ( SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import DeconzEntityHandler, get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -41,8 +41,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) - entity_handler = DeconzEntityHandler(gateway) - @callback def async_add_light(lights): """Add light from deCONZ.""" @@ -63,13 +61,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_group(groups): """Add group from deCONZ.""" + if not gateway.option_allow_deconz_groups: + return + entities = [] for group in groups: if group.lights: - new_group = DeconzGroup(group, gateway) - entity_handler.add_entity(new_group) - entities.append(new_group) + entities.append(DeconzGroup(group, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 0792e436321..c32b26f299d 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice from .deconz_event import DeconzEvent -from .gateway import DeconzEntityHandler, get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry ATTR_CURRENT = "current" ATTR_POWER = "power" @@ -37,7 +37,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): batteries = set() battery_handler = DeconzBatteryHandler(gateway) - entity_handler = DeconzEntityHandler(gateway) @callback def async_add_sensor(sensors, new=True): @@ -65,11 +64,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): new and sensor.BINARY is False and sensor.type not in Battery.ZHATYPE + Thermostat.ZHATYPE + and ( + gateway.option_allow_clip_sensor + or not sensor.type.startswith("CLIP") + ) ): - - new_sensor = DeconzSensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + entities.append(DeconzSensor(sensor, gateway)) if sensor.battery is not None: new_battery = DeconzBattery(sensor, gateway) From 431a3a6b4471441bc41279818d86d0a32bb256cd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Feb 2020 11:04:17 +0100 Subject: [PATCH 113/378] Re-branding of Hass.io panel to Supervisor (#31480) --- homeassistant/components/hassio/__init__.py | 2 +- tests/components/hassio/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index f70e44cfa55..cc03f26085c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -194,7 +194,7 @@ async def async_setup(hass, config): await hass.components.panel_custom.async_register_panel( frontend_url_path="hassio", webcomponent_name="hassio-main", - sidebar_title="Hass.io", + sidebar_title="Supervisor", sidebar_icon="hass:home-assistant", js_url="/api/hassio/app/entrypoint.js", embed_iframe=True, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 1e227f943ed..2751062dedf 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -52,7 +52,7 @@ async def test_setup_api_panel(hass, aioclient_mock): assert panels.get("hassio").to_response() == { "component_name": "custom", "icon": "hass:home-assistant", - "title": "Hass.io", + "title": "Supervisor", "url_path": "hassio", "require_admin": True, "config": { From 67680bcfa8f436e1cace14a4b695da0ccabff3d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 07:52:21 -0800 Subject: [PATCH 114/378] Automation device/entity extraction to include triggers + conditions (#31474) * Add support for extracting triggers * Add support for extracting triggers * Fix test --- .../components/automation/__init__.py | 172 ++++++++++++------ tests/components/automation/test_init.py | 24 ++- tests/components/search/test_init.py | 4 +- 3 files changed, 143 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 45f892d783e..528a314dd7b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,16 +1,19 @@ """Allow to set up simple automation rules via the config file.""" -from functools import partial import importlib import logging -from typing import Any, Awaitable, Callable, List +from typing import Any, Awaitable, Callable, List, Optional, Set import voluptuous as vol +from homeassistant.components import sun from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + CONF_DEVICE_ID, + CONF_ENTITY_ID, CONF_ID, CONF_PLATFORM, + CONF_ZONE, EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, @@ -130,7 +133,7 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: results = [] for automation_entity in component.entities: - if entity_id in automation_entity.action_script.referenced_entities: + if entity_id in automation_entity.referenced_entities: results.append(automation_entity.entity_id) return results @@ -149,7 +152,7 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: if automation_entity is None: return [] - return list(automation_entity.action_script.referenced_entities) + return list(automation_entity.referenced_entities) @callback @@ -163,7 +166,7 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: results = [] for automation_entity in component.entities: - if device_id in automation_entity.action_script.referenced_devices: + if device_id in automation_entity.referenced_devices: results.append(automation_entity.entity_id) return results @@ -182,7 +185,7 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: if automation_entity is None: return [] - return list(automation_entity.action_script.referenced_devices) + return list(automation_entity.referenced_devices) async def async_setup(hass, config): @@ -232,7 +235,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self, automation_id, name, - async_attach_triggers, + trigger_config, cond_func, action_script, hidden, @@ -241,7 +244,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Initialize an automation entity.""" self._id = automation_id self._name = name - self._async_attach_triggers = async_attach_triggers + self._trigger_config = trigger_config self._async_detach_triggers = None self._cond_func = cond_func self.action_script = action_script @@ -249,6 +252,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._hidden = hidden self._initial_state = initial_state self._is_enabled = False + self._referenced_entities: Optional[Set[str]] = None + self._referenced_devices: Optional[Set[str]] = None @property def name(self): @@ -280,6 +285,45 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None or self._is_enabled + @property + def referenced_devices(self): + """Return a set of referenced devices.""" + if self._referenced_devices is not None: + return self._referenced_devices + + referenced = self.action_script.referenced_devices + + if self._cond_func is not None: + for conf in self._cond_func.config: + referenced |= condition.async_extract_devices(conf) + + for conf in self._trigger_config: + device = _trigger_extract_device(conf) + if device is not None: + referenced.add(device) + + self._referenced_devices = referenced + return referenced + + @property + def referenced_entities(self): + """Return a set of referenced entities.""" + if self._referenced_entities is not None: + return self._referenced_entities + + referenced = self.action_script.referenced_entities + + if self._cond_func is not None: + for conf in self._cond_func.config: + referenced |= condition.async_extract_entities(conf) + + for conf in self._trigger_config: + for entity_id in _trigger_extract_entities(conf): + referenced.add(entity_id) + + self._referenced_entities = referenced + return referenced + async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" await super().async_added_to_hass() @@ -330,7 +374,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): This method is a coroutine. """ - if not skip_condition and not self._cond_func(variables): + if ( + not skip_condition + and self._cond_func is not None + and not self._cond_func(variables) + ): return # Create a new context referring to the old context. @@ -373,9 +421,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): # HomeAssistant is starting up if self.hass.state != CoreState.not_running: - self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger - ) + self._async_detach_triggers = await self._async_attach_triggers() self.async_write_ha_state() return @@ -385,9 +431,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): if not self._is_enabled or self._async_detach_triggers is not None: return - self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger - ) + self._async_detach_triggers = await self._async_attach_triggers() self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, async_enable_automation @@ -407,6 +451,38 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.async_write_ha_state() + async def _async_attach_triggers(self): + """Set up the triggers.""" + removes = [] + info = {"name": self._name} + + for conf in self._trigger_config: + platform = importlib.import_module( + ".{}".format(conf[CONF_PLATFORM]), __name__ + ) + + remove = await platform.async_attach_trigger( + self.hass, conf, self.async_trigger, info + ) + + if not remove: + _LOGGER.error("Error setting up trigger %s", self._name) + continue + + _LOGGER.info("Initialized trigger %s", self._name) + removes.append(remove) + + if not removes: + return None + + @callback + def remove_triggers(): + """Remove attached triggers.""" + for remove in removes: + remove() + + return remove_triggers + @property def device_state_attributes(self): """Return automation attributes.""" @@ -441,22 +517,12 @@ async def _async_process_config(hass, config, component): if cond_func is None: continue else: + cond_func = None - def cond_func(variables): - """Condition will always pass.""" - return True - - async_attach_triggers = partial( - _async_process_trigger, - hass, - config, - config_block.get(CONF_TRIGGER, []), - name, - ) entity = AutomationEntity( automation_id, name, - async_attach_triggers, + config_block[CONF_TRIGGER], cond_func, action_script, hidden, @@ -471,7 +537,7 @@ async def _async_process_config(hass, config, component): async def _async_process_if(hass, config, p_config): """Process if checks.""" - if_configs = p_config.get(CONF_CONDITION) + if_configs = p_config[CONF_CONDITION] checks = [] for if_config in if_configs: @@ -485,35 +551,33 @@ async def _async_process_if(hass, config, p_config): """AND all conditions.""" return all(check(hass, variables) for check in checks) + if_action.config = if_configs + return if_action -async def _async_process_trigger(hass, config, trigger_configs, name, action): - """Set up the triggers. - - This method is a coroutine. - """ - removes = [] - info = {"name": name} - - for conf in trigger_configs: - platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - - remove = await platform.async_attach_trigger(hass, conf, action, info) - - if not remove: - _LOGGER.error("Error setting up trigger %s", name) - continue - - _LOGGER.info("Initialized trigger %s", name) - removes.append(remove) - - if not removes: +@callback +def _trigger_extract_device(trigger_conf: dict) -> Optional[str]: + """Extract devices from a trigger config.""" + if trigger_conf[CONF_PLATFORM] != "device": return None - def remove_triggers(): - """Remove attached triggers.""" - for remove in removes: - remove() + return trigger_conf[CONF_DEVICE_ID] - return remove_triggers + +@callback +def _trigger_extract_entities(trigger_conf: dict) -> List[str]: + """Extract entities from a trigger config.""" + if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): + return trigger_conf[CONF_ENTITY_ID] + + if trigger_conf[CONF_PLATFORM] == "zone": + return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] + + if trigger_conf[CONF_PLATFORM] == "geo_location": + return [trigger_conf[CONF_ZONE]] + + if trigger_conf[CONF_PLATFORM] == "sun": + return [sun.ENTITY_ID] + + return [] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 391c9646dd4..c27a0262a4e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -935,6 +935,11 @@ async def test_extraction_functions(hass): { "alias": "test1", "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "condition": { + "condition": "state", + "entity_id": "light.condition_state", + "state": "on", + }, "action": [ { "service": "test.script", @@ -954,7 +959,20 @@ async def test_extraction_functions(hass): }, { "alias": "test2", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_2"}, + "trigger": { + "platform": "device", + "domain": "light", + "type": "turned_on", + "entity_id": "light.trigger_2", + "device_id": "trigger-device-2", + }, + "condition": { + "condition": "device", + "device_id": "condition-device", + "domain": "light", + "type": "is_on", + "entity_id": "light.bla", + }, "action": [ { "service": "test.script", @@ -989,6 +1007,8 @@ async def test_extraction_functions(hass): "automation.test2", } assert set(automation.entities_in_automation(hass, "automation.test1")) == { + "sensor.trigger_1", + "light.condition_state", "light.in_both", "light.in_first", } @@ -997,6 +1017,8 @@ async def test_extraction_functions(hass): "automation.test2", } assert set(automation.devices_in_automation(hass, "automation.test2")) == { + "trigger-device-2", + "condition-device", "device-in-both", "device-in-last", } diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index 54a32bed229..a379b91f82a 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -163,7 +163,7 @@ async def test_search(hass): "automation": [ { "alias": "wled_entity", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "trigger": {"platform": "template", "value_template": "true"}, "action": [ { "service": "test.script", @@ -173,7 +173,7 @@ async def test_search(hass): }, { "alias": "wled_device", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "trigger": {"platform": "template", "value_template": "true"}, "action": [ { "domain": "light", From 84cbcb4d16da39b7bd3c48721f1f5d05123596d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 09:00:20 -0800 Subject: [PATCH 115/378] Remove tests for deprecated key (#31491) --- .../components/google_assistant/test_http.py | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 112935f0160..f5e3e505a28 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -145,38 +145,6 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): assert call[3] == MOCK_HEADER -async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): - """Test the function to call the homegraph api.""" - config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), - ) - await config.async_initialize() - - aioclient_mock.post(MOCK_URL, status=200, json={}) - - res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) - assert res == 200 - assert aioclient_mock.call_count == 1 - - call = aioclient_mock.mock_calls[0] - assert call[1].query == {"key": "dummy_key"} - assert call[2] == MOCK_JSON - - -async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): - """Test the function to call the homegraph api.""" - config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), - ) - await config.async_initialize() - - aioclient_mock.post(MOCK_URL, status=666, json={}) - - res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) - assert res == 666 - assert aioclient_mock.call_count == 1 - - async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" agent_user_id = "user" From 0f56fc75b3d2f4e8ce8cba5892ef5d8725dd5a97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 09:00:41 -0800 Subject: [PATCH 116/378] Bump version to 0.106.0dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ee2c4767ba9..e56ca49b389 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 105 +MINOR_VERSION = 106 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 472fe7a0fa91a7a7da8d388ef0c031ac29f4d524 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 09:50:00 -0800 Subject: [PATCH 117/378] Fix Google API key test (#31492) --- tests/components/google_assistant/test_init.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 2773f3c3329..0df2b032b5a 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -3,17 +3,21 @@ from homeassistant.components import google_assistant as ga from homeassistant.core import Context from homeassistant.setup import async_setup_component -GA_API_KEY = "Agdgjsj399sdfkosd932ksd" +from .test_http import DUMMY_CONFIG async def test_request_sync_service(aioclient_mock, hass): """Test that it posts to the request_sync url.""" + aioclient_mock.post( + ga.const.HOMEGRAPH_TOKEN_URL, + status=200, + json={"access_token": "1234", "expires_in": 3600}, + ) + aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=200) await async_setup_component( - hass, - "google_assistant", - {"google_assistant": {"project_id": "test_project", "api_key": GA_API_KEY}}, + hass, "google_assistant", {"google_assistant": DUMMY_CONFIG}, ) assert aioclient_mock.call_count == 0 @@ -24,4 +28,4 @@ async def test_request_sync_service(aioclient_mock, hass): context=Context(user_id="123"), ) - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 # token + request From 557f5763dffead7b2b3353553184bda3686b2a2d Mon Sep 17 00:00:00 2001 From: dupondje Date: Wed, 5 Feb 2020 22:14:03 +0100 Subject: [PATCH 118/378] Add belgian meter and rename some dsmr sensors (#30121) * Add support for belgian meter and rename some sensors * DSMR Fixes * Add test * More tests * Adjust test to latest dev * Remove unused code * Depend on dsmr_parser 0.18 --- homeassistant/components/dsmr/sensor.py | 35 ++++--- tests/components/dsmr/test_sensor.py | 132 +++++++++++++++++++++++- 2 files changed, 153 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 253e8409f1b..86a3b0b6fbc 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,6 +1,5 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" import asyncio -from datetime import timedelta from functools import partial import logging @@ -32,9 +31,6 @@ ICON_POWER = "mdi:flash" ICON_POWER_FAILURE = "mdi:flash-off" ICON_SWELL_SAG = "mdi:pulse" -# Smart meter sends telegram every 10 seconds -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - RECONNECT_INTERVAL = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -42,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(["5", "4", "2.2"]) + cv.string, vol.In(["5B", "5", "4", "2.2"]) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), @@ -62,17 +58,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], - ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], - ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], - ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ["Power Production (normal)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + ["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], + ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1], + ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2], + ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], + ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], ["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], ["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], ["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], ["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], ["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], + ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT], ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT], ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT], ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT], @@ -83,6 +80,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1], ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2], ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3], + ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1], + ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2], + ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], ] # Generate device entities @@ -91,6 +91,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Protocol version specific obis if dsmr_version in ("4", "5"): gas_obis = obis_ref.HOURLY_GAS_METER_READING + elif dsmr_version in ("5B"): + gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING else: gas_obis = obis_ref.GAS_METER_READING @@ -214,7 +216,7 @@ class DSMREntity(Entity): value = self.get_dsmr_object_attr("value") if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: - return self.translate_tariff(value) + return self.translate_tariff(value, self._config[CONF_DSMR_VERSION]) try: value = round(float(value), self._config[CONF_PRECISION]) @@ -232,8 +234,15 @@ class DSMREntity(Entity): return self.get_dsmr_object_attr("unit") @staticmethod - def translate_tariff(value): - """Convert 2/1 to normal/low.""" + def translate_tariff(value, dsmr_version): + """Convert 2/1 to normal/low depening on DSMR version.""" + # DSMR V5B: Note: In Belgium values are swapped: + # Rate code 2 is used for low rate and rate code 1 is used for normal rate. + if dsmr_version in ("5B"): + if value == "0001": + value = "0002" + elif value == "0002": + value = "0001" # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is # used for normal rate. if value == "0002": diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 81249c04046..c881f4b9168 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -52,8 +52,9 @@ async def test_default_setup(hass, mock_connection_factory): from dsmr_parser.obis_references import ( CURRENT_ELECTRICITY_USAGE, ELECTRICITY_ACTIVE_TARIFF, + GAS_METER_READING, ) - from dsmr_parser.objects import CosemObject + from dsmr_parser.objects import CosemObject, MBusObject config = {"platform": "dsmr"} @@ -62,6 +63,12 @@ async def test_default_setup(hass, mock_connection_factory): [{"value": Decimal("0.0"), "unit": "kWh"}] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), + GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ] + ), } with assert_setup_component(1): @@ -90,6 +97,11 @@ async def test_default_setup(hass, mock_connection_factory): assert power_tariff.state == "low" assert power_tariff.attributes.get("unit_of_measurement") == "" + # check if gas consumption is parsed correctly + gas_consumption = hass.states.get("sensor.gas_consumption") + assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get("unit_of_measurement") == "m3" + async def test_derivative(): """Test calculation of derivative value.""" @@ -131,6 +143,124 @@ async def test_derivative(): assert entity.unit_of_measurement == "m3/h" +async def test_v4_meter(hass, mock_connection_factory): + """Test if v4 meter is correctly parsed.""" + (connection_factory, transport, protocol) = mock_connection_factory + + from dsmr_parser.obis_references import ( + HOURLY_GAS_METER_READING, + ELECTRICITY_ACTIVE_TARIFF, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + config = {"platform": "dsmr", "dsmr_version": "4"} + + telegram = { + HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ] + ), + ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), + } + + with assert_setup_component(1): + await async_setup_component(hass, "sensor", {"sensor": config}) + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get("sensor.power_tariff") + assert power_tariff.state == "low" + assert power_tariff.attributes.get("unit_of_measurement") == "" + + # check if gas consumption is parsed correctly + gas_consumption = hass.states.get("sensor.gas_consumption") + assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get("unit_of_measurement") == "m3" + + +async def test_belgian_meter(hass, mock_connection_factory): + """Test if Belgian meter is correctly parsed.""" + (connection_factory, transport, protocol) = mock_connection_factory + + from dsmr_parser.obis_references import ( + BELGIUM_HOURLY_GAS_METER_READING, + ELECTRICITY_ACTIVE_TARIFF, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + config = {"platform": "dsmr", "dsmr_version": "5B"} + + telegram = { + BELGIUM_HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ] + ), + ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), + } + + with assert_setup_component(1): + await async_setup_component(hass, "sensor", {"sensor": config}) + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get("sensor.power_tariff") + assert power_tariff.state == "normal" + assert power_tariff.attributes.get("unit_of_measurement") == "" + + # check if gas consumption is parsed correctly + gas_consumption = hass.states.get("sensor.gas_consumption") + assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get("unit_of_measurement") == "m3" + + +async def test_belgian_meter_low(hass, mock_connection_factory): + """Test if Belgian meter is correctly parsed.""" + (connection_factory, transport, protocol) = mock_connection_factory + + from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF + from dsmr_parser.objects import CosemObject + + config = {"platform": "dsmr", "dsmr_version": "5B"} + + telegram = { + ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}]), + } + + with assert_setup_component(1): + await async_setup_component(hass, "sensor", {"sensor": config}) + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get("sensor.power_tariff") + assert power_tariff.state == "low" + assert power_tariff.attributes.get("unit_of_measurement") == "" + + async def test_tcp(hass, mock_connection_factory): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = mock_connection_factory From 481ea0aa5be3c1a6dd91b783c2f34d87757525da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 13:57:17 -0800 Subject: [PATCH 119/378] Check for known Hue vulnerability (#31494) --- homeassistant/components/hue/__init__.py | 17 ++++++++++-- tests/components/hue/test_init.py | 34 +++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index c8864e97607..ff51fc667e6 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -6,6 +6,7 @@ from aiohue.util import normalize_bridge_id import voluptuous as vol from homeassistant import config_entries, core +from homeassistant.components import persistent_notification from homeassistant.const import CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -142,8 +143,20 @@ async def async_setup_entry( sw_version=config.swversion, ) - if config.swupdate2_bridge_state == "readytoinstall": - err = "Please check for software updates of the bridge in the Philips Hue App." + if config.modelid == "BSB002" and config.swversion < "1935144040": + persistent_notification.async_create( + hass, + "Your Hue hub has a known security vulnerability ([CVE-2020-6007](https://cve.circl.lu/cve/CVE-2020-6007)). Go to the Hue app and check for software updates.", + "Signify Hue", + "hue_hub_firmware", + ) + + elif config.swupdate2_bridge_state == "readytoinstall": + err = ( + "Please check for software updates of the bridge in the Philips Hue App.", + "Signify Hue", + "hue_hub_firmware", + ) _LOGGER.warning(err) return True diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 35e1ba689b4..375d5da4456 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,5 +1,7 @@ """Test Hue setup process.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock + +from asynctest import CoroutineMock, patch from homeassistant.components import hue from homeassistant.setup import async_setup_component @@ -184,3 +186,33 @@ async def test_setting_unique_id(hass): assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert entry.unique_id == "mock-id" + + +async def test_security_vuln_check(hass): + """Test that we report security vulnerabilities.""" + assert await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) + entry.add_to_hass(hass) + + with patch.object( + hue, + "HueBridge", + Mock( + return_value=Mock( + async_setup=CoroutineMock(return_value=True), + api=Mock( + config=Mock( + bridgeid="", mac="", modelid="BSB002", swversion="1935144020" + ) + ), + ) + ), + ): + + assert await async_setup_component(hass, "hue", {}) + + await hass.async_block_till_done() + + state = hass.states.get("persistent_notification.hue_hub_firmware") + assert state is not None + assert "CVE-2020-6007" in state.attributes["message"] From ba9892e1f1dcca5806777ef2370ad3f5b790ec8e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 14:41:01 -0800 Subject: [PATCH 120/378] Updated frontend to 20200130.2 (#31502) --- 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 09bd35ba89b..8c54d20429c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200130.1" + "home-assistant-frontend==20200130.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3e4c1cf581b..c638f6e2fa7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200130.1 +home-assistant-frontend==20200130.2 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index a427826d861..23240c7399f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.1 +home-assistant-frontend==20200130.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 904df51f68f..2700526a00c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.1 +home-assistant-frontend==20200130.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From c9be201ee2fefd2842cec9c65a4f70ae60f3cc2d Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2020 16:44:44 -0600 Subject: [PATCH 121/378] Move program_mode check (#31501) Don't try to capture program_mode unless ct80 --- homeassistant/components/radiotherm/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index a6beeaa187b..cba7a736df2 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -290,6 +290,8 @@ class RadioThermostat(ClimateDevice): ) return self._current_humidity = humiditydata + self._program_mode = data["program_mode"] + self._preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] # Map thermostat values into various STATE_ flags. self._current_temperature = current_temp @@ -297,8 +299,6 @@ class RadioThermostat(ClimateDevice): self._fstate = CODE_TO_FAN_STATE[data["fstate"]] self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] - self._program_mode = data["program_mode"] - self._preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] self._current_operation = self._tmode if self._tmode == HVAC_MODE_COOL: From 41c55e695edde7cd3bb157b3df6a42648734f7f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 14:45:14 -0800 Subject: [PATCH 122/378] Fix typo in comment --- homeassistant/components/dsmr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 86a3b0b6fbc..54c8e3e29b2 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -235,7 +235,7 @@ class DSMREntity(Entity): @staticmethod def translate_tariff(value, dsmr_version): - """Convert 2/1 to normal/low depening on DSMR version.""" + """Convert 2/1 to normal/low depending on DSMR version.""" # DSMR V5B: Note: In Belgium values are swapped: # Rate code 2 is used for low rate and rate code 1 is used for normal rate. if dsmr_version in ("5B"): From 8d2086d07605469c07876accf9c8db7272df1997 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 15:50:20 -0800 Subject: [PATCH 123/378] Sonos services to work without admin access (#31506) --- homeassistant/components/sonos/media_player.py | 15 +++++++-------- tests/components/sonos/conftest.py | 2 +- tests/components/sonos/test_media_player.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index bcdb74ad438..97d03e2116e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -174,6 +174,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): platform = entity_platform.current_platform.get() + @service.verify_domain_control(hass, SONOS_DOMAIN) async def async_service_handle(service_call: ServiceCall): """Handle dispatched services.""" entities = await platform.async_extract_from_service(service_call) @@ -201,16 +202,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, entities, service_call.data[ATTR_WITH_GROUP] ) - service.async_register_admin_service( - hass, + hass.services.async_register( SONOS_DOMAIN, SERVICE_JOIN, async_service_handle, cv.make_entity_service_schema({vol.Required(ATTR_MASTER): cv.entity_id}), ) - service.async_register_admin_service( - hass, + hass.services.async_register( SONOS_DOMAIN, SERVICE_UNJOIN, async_service_handle, @@ -221,12 +220,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): {vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean} ) - service.async_register_admin_service( - hass, SONOS_DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema + hass.services.async_register( + SONOS_DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema ) - service.async_register_admin_service( - hass, SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema + hass.services.async_register( + SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema ) platform.async_register_entity_service( diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index e0257585ad5..246d1eb1627 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -33,7 +33,7 @@ def soco_fixture(music_library, speaker_info, dummy_soco_service): yield mock_soco -@pytest.fixture(name="discover") +@pytest.fixture(name="discover", autouse=True) def discover_fixture(soco): """Create a mock pysonos discover fixture.""" diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d21d3f01792..5014ded96bb 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,9 @@ """Tests for the Sonos Media Player platform.""" +import pytest + from homeassistant.components.sonos import DOMAIN, media_player +from homeassistant.core import Context +from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component @@ -24,3 +28,17 @@ async def test_async_setup_entry_discover(hass, config_entry, discover): entity = hass.data[media_player.DATA_SONOS].entities[0] assert entity.unique_id == "RINCON_test" + + +async def test_services(hass, config_entry, config, hass_read_only_user): + """Test join/unjoin requires control access.""" + await setup_platform(hass, config_entry, config) + + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + media_player.SERVICE_JOIN, + {"master": "media_player.bla", "entity_id": "media_player.blub"}, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) From 6f99bac5571bdafa764f97405b986e3dd67b72bd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 6 Feb 2020 00:31:53 +0000 Subject: [PATCH 124/378] [ci skip] Translation update --- .../garmin_connect/.translations/pl.json | 2 +- .../garmin_connect/.translations/sv.json | 4 +- .../meteo_france/.translations/ca.json | 13 ++++ .../meteo_france/.translations/da.json | 18 ++++++ .../meteo_france/.translations/ko.json | 18 ++++++ .../meteo_france/.translations/lb.json | 18 ++++++ .../meteo_france/.translations/no.json | 18 ++++++ .../meteo_france/.translations/pl.json | 18 ++++++ .../meteo_france/.translations/ru.json | 18 ++++++ .../meteo_france/.translations/sv.json | 18 ++++++ .../meteo_france/.translations/zh-Hant.json | 18 ++++++ .../components/mikrotik/.translations/en.json | 64 +++++++++---------- .../components/mikrotik/.translations/lb.json | 37 +++++++++++ .../components/mikrotik/.translations/pl.json | 6 +- .../components/mikrotik/.translations/sv.json | 10 +++ .../components/netatmo/.translations/sv.json | 9 ++- .../components/ring/.translations/sv.json | 2 +- .../samsungtv/.translations/lb.json | 2 + .../samsungtv/.translations/pl.json | 4 +- .../samsungtv/.translations/sv.json | 16 ++++- .../components/spotify/.translations/sv.json | 2 +- .../components/vizio/.translations/da.json | 1 + .../components/vizio/.translations/ko.json | 1 + .../components/vizio/.translations/lb.json | 1 + .../components/vizio/.translations/no.json | 1 + .../components/vizio/.translations/pl.json | 1 + .../components/vizio/.translations/ru.json | 1 + .../vizio/.translations/zh-Hant.json | 1 + 28 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/meteo_france/.translations/ca.json create mode 100644 homeassistant/components/meteo_france/.translations/da.json create mode 100644 homeassistant/components/meteo_france/.translations/ko.json create mode 100644 homeassistant/components/meteo_france/.translations/lb.json create mode 100644 homeassistant/components/meteo_france/.translations/no.json create mode 100644 homeassistant/components/meteo_france/.translations/pl.json create mode 100644 homeassistant/components/meteo_france/.translations/ru.json create mode 100644 homeassistant/components/meteo_france/.translations/sv.json create mode 100644 homeassistant/components/meteo_france/.translations/zh-Hant.json create mode 100644 homeassistant/components/mikrotik/.translations/lb.json create mode 100644 homeassistant/components/mikrotik/.translations/sv.json diff --git a/homeassistant/components/garmin_connect/.translations/pl.json b/homeassistant/components/garmin_connect/.translations/pl.json index df80f3466e9..45d0296b668 100644 --- a/homeassistant/components/garmin_connect/.translations/pl.json +++ b/homeassistant/components/garmin_connect/.translations/pl.json @@ -19,6 +19,6 @@ "title": "Garmin Connect" } }, - "title": "Gar min Connect" + "title": "Garmin Connect" } } \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/sv.json b/homeassistant/components/garmin_connect/.translations/sv.json index 5426ce61bb4..12715a97ebe 100644 --- a/homeassistant/components/garmin_connect/.translations/sv.json +++ b/homeassistant/components/garmin_connect/.translations/sv.json @@ -16,9 +16,9 @@ "username": "Anv\u00e4ndarnamn" }, "description": "Ange dina anv\u00e4ndaruppgifter.", - "title": "" + "title": "Garmin Connect" } }, - "title": "" + "title": "Garmin Connect" } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/ca.json b/homeassistant/components/meteo_france/.translations/ca.json new file mode 100644 index 00000000000..6f2fd707045 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "city": "Ciutat" + }, + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/da.json b/homeassistant/components/meteo_france/.translations/da.json new file mode 100644 index 00000000000..7c49d6f15ee --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "By er allerede konfigureret", + "unknown": "Ukendt fejl: Pr\u00f8v igen senere" + }, + "step": { + "user": { + "data": { + "city": "By" + }, + "description": "Indtast postnummer (kun for Frankrig, anbefalet) eller bynavn", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/ko.json b/homeassistant/components/meteo_france/.translations/ko.json new file mode 100644 index 00000000000..8b2c7f49735 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\ub3c4\uc2dc\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "city": "\ub3c4\uc2dc" + }, + "description": "\uc6b0\ud3b8\ubc88\ud638 (\ud504\ub791\uc2a4) \ub610\ub294 \ub3c4\uc2dc \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" + } + }, + "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/lb.json b/homeassistant/components/meteo_france/.translations/lb.json new file mode 100644 index 00000000000..e2ee25882be --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Stad ass scho konfigur\u00e9iert", + "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol" + }, + "step": { + "user": { + "data": { + "city": "Stad" + }, + "description": "Gitt de Postcode an (n\u00ebmme fir Frankr\u00e4ich, recommand\u00e9iert) oder den Numm vun der Stad", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/no.json b/homeassistant/components/meteo_france/.translations/no.json new file mode 100644 index 00000000000..1de1094f0a5 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Byen er allerede konfigurert", + "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere" + }, + "step": { + "user": { + "data": { + "city": "By" + }, + "description": "Skriv inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/pl.json b/homeassistant/components/meteo_france/.translations/pl.json new file mode 100644 index 00000000000..a519eaead5d --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Miasto jest ju\u017c skonfigurowane", + "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej" + }, + "step": { + "user": { + "data": { + "city": "Miasto" + }, + "description": "Wprowad\u017a kod pocztowy (tylko dla Francji, zalecane) lub nazw\u0119 miasta", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/ru.json b/homeassistant/components/meteo_france/.translations/ru.json new file mode 100644 index 00000000000..6aaff5f723f --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c\u0438 \u0436\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435." + }, + "step": { + "user": { + "data": { + "city": "\u0413\u043e\u0440\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0438\u0438) \u0438\u043b\u0438 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u043e\u0440\u043e\u0434\u0430", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/sv.json b/homeassistant/components/meteo_france/.translations/sv.json new file mode 100644 index 00000000000..a7d021066d5 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Staden har redan konfigurerats", + "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" + }, + "step": { + "user": { + "data": { + "city": "Stad" + }, + "description": "Ange postnumret (endast f\u00f6r Frankrike, rekommenderat) eller ortsnamn", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/zh-Hant.json b/homeassistant/components/meteo_france/.translations/zh-Hant.json new file mode 100644 index 00000000000..d3a35a6c713 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u57ce\u5e02\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66" + }, + "step": { + "user": { + "data": { + "city": "\u57ce\u5e02\u540d\u7a31" + }, + "description": "\u8f38\u5165\u90f5\u905e\u5340\u865f\uff08\u50c5\u652f\u63f4\u6cd5\u570b\uff09\u6216\u57ce\u5e02\u540d\u7a31", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/en.json b/homeassistant/components/mikrotik/.translations/en.json index cf766345723..0423401bf83 100644 --- a/homeassistant/components/mikrotik/.translations/en.json +++ b/homeassistant/components/mikrotik/.translations/en.json @@ -1,37 +1,37 @@ { - "config": { - "abort": { - "already_configured": "Mikrotik is already configured" - }, - "error": { - "cannot_connect": "Connection Unsuccessful", - "name_exists": "Name exists", - "wrong_credentials": "Wrong Credentials" - }, - "step": { - "user": { - "data": { - "host": "Host", - "name": "Name", - "password": "Password", - "port": "Port", - "username": "Username", - "verify_ssl": "Use ssl" + "config": { + "abort": { + "already_configured": "Mikrotik is already configured" }, - "title": "Set up Mikrotik Router" - } + "error": { + "cannot_connect": "Connection Unsuccessful", + "name_exists": "Name exists", + "wrong_credentials": "Wrong Credentials" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "username": "Username", + "verify_ssl": "Use ssl" + }, + "title": "Set up Mikrotik Router" + } + }, + "title": "Mikrotik" }, - "title": "Mikrotik" - }, - "options": { - "step": { - "device_tracker": { - "data": { - "arp_ping": "Enable ARP ping", - "detection_time": "Consider home interval", - "force_dhcp": "Force scanning using DHCP" + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Enable ARP ping", + "detection_time": "Consider home interval", + "force_dhcp": "Force scanning using DHCP" + } + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/lb.json b/homeassistant/components/mikrotik/.translations/lb.json new file mode 100644 index 00000000000..2f11bad696b --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/lb.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Verbindung net erfollegr\u00e4ich", + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_credentials": "Falsh Login Informatiounen" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm", + "verify_ssl": "SSL benotzen" + }, + "title": "Mikrotik Router ariichten" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP ping aktiv\u00e9ieren", + "detection_time": "Home Intervall betruechten", + "force_dhcp": "Scannen erzw\u00e9ngen mat DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/pl.json b/homeassistant/components/mikrotik/.translations/pl.json index 0f0a587a573..1971f1866e1 100644 --- a/homeassistant/components/mikrotik/.translations/pl.json +++ b/homeassistant/components/mikrotik/.translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", - "name_exists": "Nazwa istnieje", + "name_exists": "Nazwa ju\u017c istnieje", "wrong_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, "step": { @@ -16,7 +16,7 @@ "password": "Has\u0142o", "port": "Port", "username": "Nazwa u\u017cytkownika", - "verify_ssl": "U\u017cyj ssl" + "verify_ssl": "U\u017cyj SSL" }, "title": "Skonfiguruj router Mikrotik" } @@ -28,7 +28,7 @@ "device_tracker": { "data": { "arp_ping": "W\u0142\u0105cz ping ARP", - "detection_time": "Rozwa\u017c interwa\u0142 domowy", + "detection_time": "Czas przed oznaczeniem \"poza domem\"", "force_dhcp": "Wymu\u015b skanowanie przy u\u017cyciu DHCP" } } diff --git a/homeassistant/components/mikrotik/.translations/sv.json b/homeassistant/components/mikrotik/.translations/sv.json new file mode 100644 index 00000000000..572b159221d --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/sv.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Konfigurera Mikrotik-router" + } + }, + "title": "Mikrotik" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/sv.json b/homeassistant/components/netatmo/.translations/sv.json index 29943f5e538..2047bce5b17 100644 --- a/homeassistant/components/netatmo/.translations/sv.json +++ b/homeassistant/components/netatmo/.translations/sv.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_setup": "Du kan endast konfigurera ett Netatmo-konto." + "already_setup": "Du kan endast konfigurera ett Netatmo-konto.", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Netatmo-komponenten har inte konfigurerats. F\u00f6lj dokumentationen." + }, + "create_entry": { + "default": "Autentiserad med Netatmo." }, "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" } }, - "title": "" + "title": "Netatmo" } } \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/sv.json b/homeassistant/components/ring/.translations/sv.json index fd9b66b10f0..54e9f5200f2 100644 --- a/homeassistant/components/ring/.translations/sv.json +++ b/homeassistant/components/ring/.translations/sv.json @@ -21,6 +21,6 @@ "title": "Logga in med Ring-konto" } }, - "title": "" + "title": "Ring" } } \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/lb.json b/homeassistant/components/samsungtv/.translations/lb.json index fe1f02e55ea..b3a94a1a2a6 100644 --- a/homeassistant/components/samsungtv/.translations/lb.json +++ b/homeassistant/components/samsungtv/.translations/lb.json @@ -5,8 +5,10 @@ "already_in_progress": "Konfiguratioun fir d\u00ebs Samsung TV ass schonn am gaang.", "auth_missing": "Home Assistant ass net authentifiz\u00e9iert fir sech mat d\u00ebsem Samsung TV ze verbannen.", "not_found": "Keng \u00ebnnerst\u00ebtzte Samsung TV am Netzwierk fonnt.", + "not_successful": "Keng Verbindung mat d\u00ebsem Samsung TV Apparat m\u00e9iglech.", "not_supported": "D\u00ebsen Samsung TV Modell g\u00ebtt momentan net \u00ebnnerst\u00ebtzt" }, + "flow_title": "Samsnung TV:{model}", "step": { "confirm": { "description": "W\u00ebllt dir de Samsung TV {model} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun g\u00ebtt iwwerschriwwen.", diff --git a/homeassistant/components/samsungtv/.translations/pl.json b/homeassistant/components/samsungtv/.translations/pl.json index a758c2ad379..200d8d2cf9a 100644 --- a/homeassistant/components/samsungtv/.translations/pl.json +++ b/homeassistant/components/samsungtv/.translations/pl.json @@ -5,8 +5,8 @@ "already_in_progress": "Konfiguracja telewizora Samsung jest ju\u017c w toku.", "auth_missing": "Home Assistant nie jest uwierzytelniony, aby po\u0142\u0105czy\u0107 si\u0119 z tym telewizorem Samsung.", "not_found": "W sieci nie znaleziono obs\u0142ugiwanych telewizor\u00f3w Samsung.", - "not_successful": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z tym urz\u0105dzeniem Samsung TV.", - "not_supported": "Te telewizor Samsung nie jest obecnie obs\u0142ugiwany." + "not_successful": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 urz\u0105dzeniem Samsung TV.", + "not_supported": "Ten telewizor Samsung nie jest obecnie obs\u0142ugiwany." }, "flow_title": "Samsung TV: {model}", "step": { diff --git a/homeassistant/components/samsungtv/.translations/sv.json b/homeassistant/components/samsungtv/.translations/sv.json index cf5636700aa..f75e8238506 100644 --- a/homeassistant/components/samsungtv/.translations/sv.json +++ b/homeassistant/components/samsungtv/.translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "Denna Samsung TV \u00e4r redan konfigurerad.", + "already_in_progress": "Samsung TV-konfiguration p\u00e5g\u00e5r redan.", + "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", + "not_found": "Inga Samsung TV-enheter som st\u00f6ds finns i n\u00e4tverket.", + "not_successful": "Det g\u00e5r inte att ansluta till denna Samsung TV-enhet.", + "not_supported": "Denna Samsung TV-enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte." + }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "title": "" + "description": "Vill du st\u00e4lla in Samsung TV {model}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver.", + "title": "Samsung TV" }, "user": { "data": { @@ -10,9 +20,9 @@ "name": "Namn" }, "description": "Ange informationen f\u00f6r din Samsung TV. Om du aldrig har anslutit denna till Home Assistant tidigare borde du se en popup om autentisering p\u00e5 din TV.", - "title": "" + "title": "Samsung TV" } }, - "title": "" + "title": "Samsung TV" } } \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/sv.json b/homeassistant/components/spotify/.translations/sv.json index 47e5b85c93c..5c720a1f26e 100644 --- a/homeassistant/components/spotify/.translations/sv.json +++ b/homeassistant/components/spotify/.translations/sv.json @@ -13,6 +13,6 @@ "title": "V\u00e4lj autentiseringsmetod." } }, - "title": "" + "title": "Spotify" } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json index 7a6dda98270..9ec9c4122ee 100644 --- a/homeassistant/components/vizio/.translations/da.json +++ b/homeassistant/components/vizio/.translations/da.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Denne post ser ud til allerede at v\u00e6re konfigureret med en anden v\u00e6rt og navn baseret p\u00e5 dens serienummer. Fjern eventuelle gamle poster fra din configuration.yaml og i menuen Integrationer, f\u00f8r du fors\u00f8ger at tilf\u00f8je denne enhed igen.", "host_exists": "Vizio-komponent med v\u00e6rt er allerede konfigureret.", "name_exists": "Vizio-komponent med navn er allerede konfigureret.", + "updated_entry": "Denne post er allerede konfigureret, men navnet og/eller indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med den tidligere importerede konfiguration, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed.", "updated_options": "Denne post er allerede konfigureret, men indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med de tidligere importerede indstillingsv\u00e6rdier, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed.", "updated_volume_step": "Denne post er allerede konfigureret, men lydstyrketrinst\u00f8rrelsen i konfigurationen stemmer ikke overens med konfigurationsposten, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed." }, diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json index 3e54d343f7a..4c0460ec0e1 100644 --- a/homeassistant/components/vizio/.translations/ko.json +++ b/homeassistant/components/vizio/.translations/ko.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "\uc774 \ud56d\ubaa9\uc740 \uc2dc\ub9ac\uc5bc \ubc88\ud638\ub85c \ub2e4\ub978 \ud638\uc2a4\ud2b8 \ubc0f \uc774\ub984\uc73c\ub85c \uc774\ubbf8 \uc124\uc815\ub418\uc5b4\uc788\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4. \uc774 \uae30\uae30\ub97c \ucd94\uac00\ud558\uae30 \uc804\uc5d0 configuration.yaml \ubc0f \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uba54\ub274\uc5d0\uc11c \uc774\uc804 \ud56d\ubaa9\uc744 \uc81c\uac70\ud574\uc8fc\uc138\uc694.", "host_exists": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "name_exists": "\ud574\ub2f9 \uc774\ub984\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984\uc774\ub098 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "updated_options": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uc635\uc158 \uac12\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "updated_volume_step": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc758 \ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30\uac00 \uad6c\uc131 \ud56d\ubaa9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json index 965dd7af841..809ae6d4eb5 100644 --- a/homeassistant/components/vizio/.translations/lb.json +++ b/homeassistant/components/vizio/.translations/lb.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mat engem aneren Host an Numm bas\u00e9ierend unhand vu\u00a0senger Seriennummer. L\u00e4scht w.e.g. al Entr\u00e9e vun \u00e4rer configuration.yaml a\u00a0vum Integratioun's Men\u00fc ier dir prob\u00e9iert d\u00ebsen Apparate r\u00ebm b\u00e4i ze setzen.", "host_exists": "Vizio Komponent mam Host ass schon konfigur\u00e9iert.", "name_exists": "Vizio Komponent mam Numm ass scho konfigur\u00e9iert.", + "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.", "updated_options": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9iert Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert.", "updated_volume_step": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9iert Lautst\u00e4erkt Schr\u00ebtt Gr\u00e9isst an der Konfiguratioun st\u00ebmmt net mat der Konfiguratioun iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert." }, diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index cdf16bfe28d..be1cae7aaf1 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.", "host_exists": "Vizio komponent med vert allerede konfigurert.", "name_exists": "Vizio-komponent med navn som allerede er konfigurert.", + "updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen, stemmer ikke overens med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", "updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", "updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter." }, diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index f70e6d728df..cba9f4319f5 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Wygl\u0105da na to, \u017ce ten wpis zosta\u0142 ju\u017c skonfigurowany z innym hostem i nazw\u0105 na podstawie jego numeru seryjnego. Usu\u0144 wszystkie stare wpisy z pliku configuration.yaml i z menu Integracje przed ponown\u0105 pr\u00f3b\u0105 dodania tego urz\u0105dzenia.", "host_exists": "Komponent Vizio dla tego hosta jest ju\u017c skonfigurowany.", "name_exists": "Komponent Vizio dla tej nazwy jest ju\u017c skonfigurowany.", + "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", "updated_options": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", "updated_volume_step": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale rozmiar skoku g\u0142o\u015bno\u015bci w konfiguracji nie pasuje do wpisu konfiguracji, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." }, diff --git a/homeassistant/components/vizio/.translations/ru.json b/homeassistant/components/vizio/.translations/ru.json index 2206336a5b4..e8f14e796ba 100644 --- a/homeassistant/components/vizio/.translations/ru.json +++ b/homeassistant/components/vizio/.translations/ru.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "\u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u044d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0431\u044b\u043b\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0445\u043e\u0441\u0442\u043e\u043c \u0438 \u0438\u043c\u0435\u043d\u0435\u043c \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0435\u0433\u043e \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0432\u0441\u0435 \u0441\u0442\u0430\u0440\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e configuration.yaml \u0438 \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0430 \"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\" \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "updated_options": "\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.", "updated_volume_step": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u0448\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \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\u0435\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." }, diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json index 6707a321911..cd859977551 100644 --- a/homeassistant/components/vizio/.translations/zh-Hant.json +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "\u6839\u64da\u6240\u63d0\u4f9b\u7684\u5e8f\u865f\uff0c\u6b64\u7269\u4ef6\u4f3c\u4e4e\u5df2\u7d93\u4f7f\u7528\u4e0d\u540c\u7684\u4e3b\u6a5f\u7aef\u8207\u540d\u7a31\u9032\u884c\u8a2d\u5b9a\u3002\u8acb\u5f9e\u6574\u5408\u9078\u55ae Config.yaml \u4e2d\u79fb\u9664\u820a\u7269\u4ef6\uff0c\u7136\u5f8c\u518d\u65b0\u589e\u6b64\u8a2d\u5099\u3002", "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u9078\u9805\u540d\u7a31\u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", "updated_options": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u9078\u9805\u5b9a\u7fa9\u8207\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", "updated_volume_step": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u97f3\u91cf\u5927\u5c0f\u8207\u7269\u4ef6\u8a2d\u5b9a\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, From 8d429d76762bd84df8bc0fd4e9b5657f5d53fe8e Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 6 Feb 2020 21:32:30 +1100 Subject: [PATCH 125/378] Add GDACS feed integration (#31235) * initial version of gdacs integration * updated translations * generated files * added abbreviation * bumped library version * small feed entry attribute fixes * add unit tests * need to use original mdi name * bumped library version * improved entity name for earthquakes * round vulnerability number * typo * support for categories * testing support for categories * tie longitude and latitude together * validating categories * simplifying setup * passing domain as parameter * simplified test setup * moved test code * simplified test code * removed superfluous code * changed approach to unique identifier * changed code structure * simplified unit system handling * made schema a constant * comment added * simplifying code * added message if location already configured * removed unnecessary code * simplified test code * avoid mocking __init__ * pylint * simplified code * fetch categories from integration library * setting PARALLEL_UPDATES * setting PARALLEL_UPDATES to zero/unlimited * added quality scale --- CODEOWNERS | 1 + .../components/gdacs/.translations/en.json | 16 ++ homeassistant/components/gdacs/__init__.py | 212 +++++++++++++++ homeassistant/components/gdacs/config_flow.py | 66 +++++ homeassistant/components/gdacs/const.py | 25 ++ .../components/gdacs/geo_location.py | 234 +++++++++++++++++ homeassistant/components/gdacs/manifest.json | 14 + homeassistant/components/gdacs/sensor.py | 140 ++++++++++ homeassistant/components/gdacs/strings.json | 16 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/gdacs/__init__.py | 41 +++ tests/components/gdacs/conftest.py | 31 +++ tests/components/gdacs/test_config_flow.py | 76 ++++++ tests/components/gdacs/test_geo_location.py | 242 ++++++++++++++++++ tests/components/gdacs/test_init.py | 19 ++ tests/components/gdacs/test_sensor.py | 100 ++++++++ 18 files changed, 1240 insertions(+) create mode 100644 homeassistant/components/gdacs/.translations/en.json create mode 100644 homeassistant/components/gdacs/__init__.py create mode 100644 homeassistant/components/gdacs/config_flow.py create mode 100644 homeassistant/components/gdacs/const.py create mode 100644 homeassistant/components/gdacs/geo_location.py create mode 100644 homeassistant/components/gdacs/manifest.json create mode 100644 homeassistant/components/gdacs/sensor.py create mode 100644 homeassistant/components/gdacs/strings.json create mode 100644 tests/components/gdacs/__init__.py create mode 100644 tests/components/gdacs/conftest.py create mode 100644 tests/components/gdacs/test_config_flow.py create mode 100644 tests/components/gdacs/test_geo_location.py create mode 100644 tests/components/gdacs/test_init.py create mode 100644 tests/components/gdacs/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4df7f250d60..47cc30f1117 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -117,6 +117,7 @@ homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky +homeassistant/components/gdacs/* @exxamalte homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte diff --git a/homeassistant/components/gdacs/.translations/en.json b/homeassistant/components/gdacs/.translations/en.json new file mode 100644 index 00000000000..4e7ceb3846c --- /dev/null +++ b/homeassistant/components/gdacs/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "Global Disaster Alert and Coordination System (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py new file mode 100644 index 00000000000..34f1bdc88d8 --- /dev/null +++ b/homeassistant/components/gdacs/__init__.py @@ -0,0 +1,212 @@ +"""The Global Disaster Alert and Coordination System (GDACS) integration.""" +import asyncio +from datetime import timedelta +import logging + +from aio_georss_gdacs import GdacsFeedManager +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_MILES, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM + +from .const import ( + CONF_CATEGORIES, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, + PLATFORMS, + SIGNAL_DELETE_ENTITY, + SIGNAL_NEW_GEOLOCATION, + SIGNAL_STATUS, + SIGNAL_UPDATE_ENTITY, + VALID_CATEGORIES, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_CATEGORIES, default=[]): vol.All( + cv.ensure_list, [vol.In(VALID_CATEGORIES)] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the GDACS component.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + scan_interval = conf[CONF_SCAN_INTERVAL] + categories = conf[CONF_CATEGORIES] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_SCAN_INTERVAL: scan_interval, + CONF_CATEGORIES: categories, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GDACS component as config entry.""" + hass.data.setdefault(DOMAIN, {}) + feeds = hass.data[DOMAIN].setdefault(FEED, {}) + + radius = config_entry.data[CONF_RADIUS] + if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GdacsFeedEntityManager(hass, config_entry, radius) + feeds[config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GDACS component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + await asyncio.wait( + [ + hass.config_entries.async_forward_entry_unload(config_entry, domain) + for domain in PLATFORMS + ] + ) + return True + + +class GdacsFeedEntityManager: + """Feed Entity Manager for GDACS feed.""" + + def __init__(self, hass, config_entry, radius_in_km): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + categories = config_entry.data[CONF_CATEGORIES] + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GdacsFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + filter_radius=radius_in_km, + filter_categories=categories, + status_async_callback=self._status_update, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._track_time_remove_callback = None + self._status_info = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + for domain in PLATFORMS: + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, domain + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_GEOLOCATION.format(self._config_entry_id) + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def status_info(self): + """Return latest status update info received.""" + return self._status_info + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, self.async_event_new_entity(), self, external_id + ) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Remove entity.""" + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + + async def _status_update(self, status_info): + """Propagate status update.""" + _LOGGER.debug("Status update received: %s", status_info) + self._status_info = status_info + async_dispatcher_send(self._hass, SIGNAL_STATUS.format(self._config_entry_id)) diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py new file mode 100644 index 00000000000..1e12a116ed5 --- /dev/null +++ b/homeassistant/components/gdacs/config_flow.py @@ -0,0 +1,66 @@ +"""Config flow to configure the GDACS integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, +) +from homeassistant.helpers import config_validation as cv + +from .const import ( # pylint: disable=unused-import + CONF_CATEGORIES, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) + +DATA_SCHEMA = vol.Schema( + {vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int} +) + +_LOGGER = logging.getLogger(__name__) + + +class GdacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a GDACS config flow.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors or {} + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + _LOGGER.debug("User input: %s", user_input) + if not user_input: + return await self._show_form() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + user_input[CONF_LATITUDE] = latitude + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_LONGITUDE] = longitude + + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() + + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + categories = user_input.get(CONF_CATEGORIES, []) + user_input[CONF_CATEGORIES] = categories + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/gdacs/const.py b/homeassistant/components/gdacs/const.py new file mode 100644 index 00000000000..4579304f30d --- /dev/null +++ b/homeassistant/components/gdacs/const.py @@ -0,0 +1,25 @@ +"""Define constants for the GDACS integration.""" +from datetime import timedelta + +from aio_georss_gdacs.consts import EVENT_TYPE_MAP + +DOMAIN = "gdacs" + +PLATFORMS = ("sensor", "geo_location") + +FEED = "feed" + +CONF_CATEGORIES = "categories" + +DEFAULT_ICON = "mdi:alert" +DEFAULT_RADIUS = 500.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_DELETE_ENTITY = "gdacs_delete_{}" +SIGNAL_UPDATE_ENTITY = "gdacs_update_{}" +SIGNAL_STATUS = "gdacs_status_{}" + +SIGNAL_NEW_GEOLOCATION = "gdacs_new_geolocation_{}" + +# Fetch valid categories from integration library. +VALID_CATEGORIES = list(EVENT_TYPE_MAP.values()) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py new file mode 100644 index 00000000000..34da104e093 --- /dev/null +++ b/homeassistant/components/gdacs/geo_location.py @@ -0,0 +1,234 @@ +"""Geolocation support for GDACS Feed.""" +import logging +from typing import Optional + +from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from .const import ( + DEFAULT_ICON, + DOMAIN, + FEED, + SIGNAL_DELETE_ENTITY, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_ALERT_LEVEL = "alert_level" +ATTR_COUNTRY = "country" +ATTR_DESCRIPTION = "description" +ATTR_DURATION_IN_WEEK = "duration_in_week" +ATTR_EVENT_TYPE = "event_type" +ATTR_EXTERNAL_ID = "external_id" +ATTR_FROM_DATE = "from_date" +ATTR_POPULATION = "population" +ATTR_SEVERITY = "severity" +ATTR_TO_DATE = "to_date" +ATTR_VULNERABILITY = "vulnerability" + +ICONS = { + "DR": "mdi:water-off", + "EQ": "mdi:pulse", + "FL": "mdi:home-flood", + "TC": "mdi:weather-hurricane", + "TS": "mdi:waves", + "VO": "mdi:image-filter-hdr", +} + +# An update of this entity is not making a web request, but uses internal data only. +PARALLEL_UPDATES = 0 + +SOURCE = "gdacs" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GDACS Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_geolocation(feed_manager, external_id): + """Add gelocation entity from feed.""" + new_entity = GdacsEvent(feed_manager, external_id) + _LOGGER.debug("Adding geolocation %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_geolocation + ) + ) + # Do not wait for update here so that the setup can be completed and because an + # update will fetch data from the feed via HTTP and then process that data. + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Geolocation setup done") + + +class GdacsEvent(GeolocationEvent): + """This represents an external event with GDACS feed data.""" + + def __init__(self, feed_manager, external_id): + """Initialize entity with data from feed entry.""" + self._feed_manager = feed_manager + self._external_id = external_id + self._title = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._alert_level = None + self._country = None + self._description = None + self._duration_in_week = None + self._event_type_short = None + self._event_type = None + self._from_date = None + self._to_date = None + self._population = None + self._severity = None + self._vulnerability = None + self._version = None + self._remove_signal_delete = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_delete = async_dispatcher_connect( + self.hass, + SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback, + ) + self._remove_signal_update = async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + self._remove_signal_delete() + self._remove_signal_update() + + @callback + def _delete_callback(self): + """Remove this entity.""" + self.hass.async_create_task(self.async_remove()) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GDACS feed location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.get_entry(self._external_id) + if feed_entry: + self._update_from_feed(feed_entry) + + def _update_from_feed(self, feed_entry): + """Update the internal state from the provided feed entry.""" + event_name = feed_entry.event_name + if not event_name: + # Earthquakes usually don't have an event name. + event_name = f"{feed_entry.country} ({feed_entry.event_id})" + self._title = f"{feed_entry.event_type}: {event_name}" + # Convert distance if not metric system. + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._distance = IMPERIAL_SYSTEM.length( + feed_entry.distance_to_home, LENGTH_KILOMETERS + ) + else: + self._distance = feed_entry.distance_to_home + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self._attribution = feed_entry.attribution + self._alert_level = feed_entry.alert_level + self._country = feed_entry.country + self._description = feed_entry.title + self._duration_in_week = feed_entry.duration_in_week + self._event_type_short = feed_entry.event_type_short + self._event_type = feed_entry.event_type + self._from_date = feed_entry.from_date + self._to_date = feed_entry.to_date + self._population = feed_entry.population + self._severity = feed_entry.severity + self._vulnerability = feed_entry.vulnerability + # Round vulnerability value if presented as float. + if isinstance(self._vulnerability, float): + self._vulnerability = round(self._vulnerability, 1) + self._version = feed_entry.version + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if self._event_type_short and self._event_type_short in ICONS: + return ICONS[self._event_type_short] + return DEFAULT_ICON + + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._title + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_DESCRIPTION, self._description), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_EVENT_TYPE, self._event_type), + (ATTR_ALERT_LEVEL, self._alert_level), + (ATTR_COUNTRY, self._country), + (ATTR_DURATION_IN_WEEK, self._duration_in_week), + (ATTR_FROM_DATE, self._from_date), + (ATTR_TO_DATE, self._to_date), + (ATTR_POPULATION, self._population), + (ATTR_SEVERITY, self._severity), + (ATTR_VULNERABILITY, self._vulnerability), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json new file mode 100644 index 00000000000..45105b21ab4 --- /dev/null +++ b/homeassistant/components/gdacs/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "gdacs", + "name": "Global Disaster Alert and Coordination System (GDACS)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/gdacs", + "requirements": [ + "aio_georss_gdacs==0.3" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ], + "quality_scale": "platinum" +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py new file mode 100644 index 00000000000..e58090fd165 --- /dev/null +++ b/homeassistant/components/gdacs/sensor.py @@ -0,0 +1,140 @@ +"""Feed Entity Manager Sensor support for GDACS Feed.""" +import logging +from typing import Optional + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt + +from .const import DEFAULT_ICON, DOMAIN, FEED, SIGNAL_STATUS + +_LOGGER = logging.getLogger(__name__) + +ATTR_STATUS = "status" +ATTR_LAST_UPDATE = "last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "last_update_successful" +ATTR_LAST_TIMESTAMP = "last_timestamp" +ATTR_CREATED = "created" +ATTR_UPDATED = "updated" +ATTR_REMOVED = "removed" + +DEFAULT_UNIT_OF_MEASUREMENT = "alerts" + +# An update of this entity is not making a web request, but uses internal data only. +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GDACS Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + sensor = GdacsSensor(entry.entry_id, entry.title, manager) + async_add_entities([sensor]) + _LOGGER.debug("Sensor setup done") + + +class GdacsSensor(Entity): + """This is a status sensor for the GDACS integration.""" + + def __init__(self, config_entry_id, config_title, manager): + """Initialize entity.""" + self._config_entry_id = config_entry_id + self._config_title = config_title + self._manager = manager + self._status = None + self._last_update = None + self._last_update_successful = None + self._last_timestamp = None + self._total = None + self._created = None + self._updated = None + self._removed = None + self._remove_signal_status = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_status = async_dispatcher_connect( + self.hass, + SIGNAL_STATUS.format(self._config_entry_id), + self._update_status_callback, + ) + _LOGGER.debug("Waiting for updates %s", self._config_entry_id) + # First update is manual because of how the feed entity manager is updated. + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + if self._remove_signal_status: + self._remove_signal_status() + + @callback + def _update_status_callback(self): + """Call status update method.""" + _LOGGER.debug("Received status update for %s", self._config_entry_id) + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GDACS status sensor.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._config_entry_id) + if self._manager: + status_info = self._manager.status_info() + if status_info: + self._update_from_status_info(status_info) + + def _update_from_status_info(self, status_info): + """Update the internal state from the provided information.""" + self._status = status_info.status + self._last_update = ( + dt.as_utc(status_info.last_update) if status_info.last_update else None + ) + if status_info.last_update_successful: + self._last_update_successful = dt.as_utc(status_info.last_update_successful) + else: + self._last_update_successful = None + self._last_timestamp = status_info.last_timestamp + self._total = status_info.total + self._created = status_info.created + self._updated = status_info.updated + self._removed = status_info.removed + + @property + def state(self): + """Return the state of the sensor.""" + return self._total + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"GDACS ({self._config_title})" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return DEFAULT_UNIT_OF_MEASUREMENT + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_STATUS, self._status), + (ATTR_LAST_UPDATE, self._last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), + (ATTR_LAST_TIMESTAMP, self._last_timestamp), + (ATTR_CREATED, self._created), + (ATTR_UPDATED, self._updated), + (ATTR_REMOVED, self._removed), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/gdacs/strings.json b/homeassistant/components/gdacs/strings.json new file mode 100644 index 00000000000..353b1b85634 --- /dev/null +++ b/homeassistant/components/gdacs/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "Global Disaster Alert and Coordination System (GDACS)", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius" + } + } + }, + "abort": { + "already_configured": "Location is already configured." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ea8a0a4e82d..83f7d4cfcfa 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -25,6 +25,7 @@ FLOWS = [ "emulated_roku", "esphome", "garmin_connect", + "gdacs", "geofency", "geonetnz_quakes", "geonetnz_volcano", diff --git a/requirements_all.txt b/requirements_all.txt index 23240c7399f..1a6eb4905a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -131,6 +131,9 @@ aio_geojson_geonetnz_volcano==0.5 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.1 +# homeassistant.components.gdacs +aio_georss_gdacs==0.3 + # homeassistant.components.ambient_station aioambient==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2700526a00c..b1d9d07ded1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,6 +43,9 @@ aio_geojson_geonetnz_volcano==0.5 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.1 +# homeassistant.components.gdacs +aio_georss_gdacs==0.3 + # homeassistant.components.ambient_station aioambient==1.0.2 diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py new file mode 100644 index 00000000000..6e61b86dbb7 --- /dev/null +++ b/tests/components/gdacs/__init__.py @@ -0,0 +1,41 @@ +"""Tests for the GDACS component.""" +from unittest.mock import MagicMock + + +def _generate_mock_feed_entry( + external_id, + title, + distance_to_home, + coordinates, + attribution=None, + alert_level=None, + country=None, + duration_in_week=None, + event_name=None, + event_type_short=None, + event_type=None, + from_date=None, + to_date=None, + population=None, + severity=None, + vulnerability=None, +): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.attribution = attribution + feed_entry.alert_level = alert_level + feed_entry.country = country + feed_entry.duration_in_week = duration_in_week + feed_entry.event_name = event_name + feed_entry.event_type_short = event_type_short + feed_entry.event_type = event_type + feed_entry.from_date = from_date + feed_entry.to_date = to_date + feed_entry.population = population + feed_entry.severity = severity + feed_entry.vulnerability = vulnerability + return feed_entry diff --git a/tests/components/gdacs/conftest.py b/tests/components/gdacs/conftest.py new file mode 100644 index 00000000000..47185cf5387 --- /dev/null +++ b/tests/components/gdacs/conftest.py @@ -0,0 +1,31 @@ +"""Configuration for GDACS tests.""" +import pytest + +from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry(): + """Create a mock GDACS config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + CONF_CATEGORIES: [], + }, + title="-41.2, 174.7", + unique_id="-41.2, 174.7", + ) diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py new file mode 100644 index 00000000000..f04f8158862 --- /dev/null +++ b/tests/components/gdacs/test_config_flow.py @@ -0,0 +1,76 @@ +"""Define tests for the GDACS config flow.""" +from datetime import timedelta + +from homeassistant import data_entry_flow +from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, +) + + +async def test_duplicate_error(hass, config_entry): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_LATITUDE: -41.2, CONF_LONGITUDE: 174.7, CONF_RADIUS: 25} + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +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_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_SCAN_INTERVAL: timedelta(minutes=4), + CONF_CATEGORIES: ["Drought", "Earthquake"], + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_SCAN_INTERVAL: 240.0, + CONF_CATEGORIES: ["Drought", "Earthquake"], + } + + +async def test_step_user(hass): + """Test that the user step works.""" + hass.config.latitude = -41.2 + hass.config.longitude = 174.7 + conf = {CONF_RADIUS: 25} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_SCAN_INTERVAL: 300.0, + CONF_CATEGORIES: [], + } diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py new file mode 100644 index 00000000000..c426b081e21 --- /dev/null +++ b/tests/components/gdacs/test_geo_location.py @@ -0,0 +1,242 @@ +"""The tests for the GDACS Feed integration.""" +import datetime + +from asynctest import patch + +from homeassistant.components import gdacs +from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED +from homeassistant.components.gdacs.geo_location import ( + ATTR_ALERT_LEVEL, + ATTR_COUNTRY, + ATTR_DESCRIPTION, + ATTR_DURATION_IN_WEEK, + ATTR_EVENT_TYPE, + ATTR_EXTERNAL_ID, + ATTR_FROM_DATE, + ATTR_POPULATION, + ATTR_SEVERITY, + ATTR_TO_DATE, + ATTR_VULNERABILITY, +) +from homeassistant.components.geo_location import ATTR_SOURCE +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from tests.common import async_fire_time_changed +from tests.components.gdacs import _generate_mock_feed_entry + +CONFIG = {gdacs.DOMAIN: {CONF_RADIUS: 200}} + + +async def test_setup(hass): + """Test the general setup of the integration.""" + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", + "Description 1", + 15.5, + (38.0, -3.0), + event_name="Name 1", + event_type_short="DR", + event_type="Drought", + alert_level="Alert Level 1", + country="Country 1", + attribution="Attribution 1", + from_date=datetime.datetime(2020, 1, 10, 8, 0, tzinfo=datetime.timezone.utc), + to_date=datetime.datetime(2020, 1, 20, 8, 0, tzinfo=datetime.timezone.utc), + duration_in_week=1, + population="Population 1", + severity="Severity 1", + vulnerability="Vulnerability 1", + ) + mock_entry_2 = _generate_mock_feed_entry( + "2345", + "Description 2", + 20.5, + (38.1, -3.1), + event_name="Name 2", + event_type_short="TC", + event_type="Tropical Cyclone", + ) + mock_entry_3 = _generate_mock_feed_entry( + "3456", + "Description 3", + 25.5, + (38.2, -3.2), + event_name="Name 3", + event_type_short="TC", + event_type="Tropical Cyclone", + country="Country 2", + ) + mock_entry_4 = _generate_mock_feed_entry( + "4567", "Description 4", 12.5, (38.3, -3.3) + ) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_georss_client.feed.GeoRssFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + # 3 geolocation and 1 sensor entities + assert len(all_states) == 4 + + state = hass.states.get("geo_location.drought_name_1") + assert state is not None + assert state.name == "Drought: Name 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", + ATTR_LATITUDE: 38.0, + ATTR_LONGITUDE: -3.0, + ATTR_FRIENDLY_NAME: "Drought: Name 1", + ATTR_DESCRIPTION: "Description 1", + ATTR_COUNTRY: "Country 1", + ATTR_ATTRIBUTION: "Attribution 1", + ATTR_FROM_DATE: datetime.datetime( + 2020, 1, 10, 8, 0, tzinfo=datetime.timezone.utc + ), + ATTR_TO_DATE: datetime.datetime( + 2020, 1, 20, 8, 0, tzinfo=datetime.timezone.utc + ), + ATTR_DURATION_IN_WEEK: 1, + ATTR_ALERT_LEVEL: "Alert Level 1", + ATTR_POPULATION: "Population 1", + ATTR_EVENT_TYPE: "Drought", + ATTR_SEVERITY: "Severity 1", + ATTR_VULNERABILITY: "Vulnerability 1", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "gdacs", + ATTR_ICON: "mdi:water-off", + } + assert float(state.state) == 15.5 + + state = hass.states.get("geo_location.tropical_cyclone_name_2") + assert state is not None + assert state.name == "Tropical Cyclone: Name 2" + assert state.attributes == { + ATTR_EXTERNAL_ID: "2345", + ATTR_LATITUDE: 38.1, + ATTR_LONGITUDE: -3.1, + ATTR_FRIENDLY_NAME: "Tropical Cyclone: Name 2", + ATTR_DESCRIPTION: "Description 2", + ATTR_EVENT_TYPE: "Tropical Cyclone", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "gdacs", + ATTR_ICON: "mdi:weather-hurricane", + } + assert float(state.state) == 20.5 + + state = hass.states.get("geo_location.tropical_cyclone_name_3") + assert state is not None + assert state.name == "Tropical Cyclone: Name 3" + assert state.attributes == { + ATTR_EXTERNAL_ID: "3456", + ATTR_LATITUDE: 38.2, + ATTR_LONGITUDE: -3.2, + ATTR_FRIENDLY_NAME: "Tropical Cyclone: Name 3", + ATTR_DESCRIPTION: "Description 3", + ATTR_EVENT_TYPE: "Tropical Cyclone", + ATTR_COUNTRY: "Country 2", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "gdacs", + ATTR_ICON: "mdi:weather-hurricane", + } + assert float(state.state) == 25.5 + + # Simulate an update - two existing, one new entry, one outdated entry + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3] + async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed_update.return_value = "OK_NO_DATA", None + async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, removes all entities + mock_feed_update.return_value = "ERROR", None + async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 1 + + +async def test_setup_imperial(hass): + """Test the setup of the integration using imperial unit system.""" + hass.config.units = IMPERIAL_SYSTEM + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", + "Description 1", + 15.5, + (38.0, -3.0), + event_name="Name 1", + event_type_short="DR", + event_type="Drought", + ) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_georss_client.feed.GeoRssFeed.update" + ) as mock_feed_update, patch( + "aio_georss_client.feed.GeoRssFeed.last_timestamp", create=True + ): + mock_feed_update.return_value = "OK", [mock_entry_1] + assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 2 + + # Test conversion of 200 miles to kilometers. + feeds = hass.data[DOMAIN][FEED] + assert feeds is not None + assert len(feeds) == 1 + manager = list(feeds.values())[0] + # Ensure that the filter value in km is correctly set. + assert manager._feed_manager._feed._filter_radius == 321.8688 + + state = hass.states.get("geo_location.drought_name_1") + assert state is not None + assert state.name == "Drought: Name 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", + ATTR_LATITUDE: 38.0, + ATTR_LONGITUDE: -3.0, + ATTR_FRIENDLY_NAME: "Drought: Name 1", + ATTR_DESCRIPTION: "Description 1", + ATTR_EVENT_TYPE: "Drought", + ATTR_UNIT_OF_MEASUREMENT: "mi", + ATTR_SOURCE: "gdacs", + ATTR_ICON: "mdi:water-off", + } + # 15.5km (as defined in mock entry) has been converted to 9.6mi. + assert float(state.state) == 9.6 diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py new file mode 100644 index 00000000000..40bda2a196b --- /dev/null +++ b/tests/components/gdacs/test_init.py @@ -0,0 +1,19 @@ +"""Define tests for the GDACS general setup.""" +from asynctest import patch + +from homeassistant.components.gdacs import DOMAIN, FEED + + +async def test_component_unload_config_entry(hass, config_entry): + """Test that loading and unloading of a config entry works.""" + config_entry.add_to_hass(hass) + with patch("aio_georss_gdacs.GdacsFeedManager.update") as mock_feed_manager_update: + # Load config entry. + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert mock_feed_manager_update.call_count == 1 + assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py new file mode 100644 index 00000000000..5e8fd5ad30f --- /dev/null +++ b/tests/components/gdacs/test_sensor.py @@ -0,0 +1,100 @@ +"""The tests for the GDACS Feed integration.""" +from asynctest import patch + +from homeassistant.components import gdacs +from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL +from homeassistant.components.gdacs.sensor import ( + ATTR_CREATED, + ATTR_LAST_UPDATE, + ATTR_LAST_UPDATE_SUCCESSFUL, + ATTR_REMOVED, + ATTR_STATUS, + ATTR_UPDATED, +) +from homeassistant.const import ( + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.gdacs import _generate_mock_feed_entry + +CONFIG = {gdacs.DOMAIN: {CONF_RADIUS: 200}} + + +async def test_setup(hass): + """Test the general setup of the integration.""" + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", "Title 1", 15.5, (38.0, -3.0), attribution="Attribution 1", + ) + mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 20.5, (38.1, -3.1),) + mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (38.2, -3.2),) + mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (38.3, -3.3)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_georss_client.feed.GeoRssFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + # 3 geolocation and 1 sensor entities + assert len(all_states) == 4 + + state = hass.states.get("sensor.gdacs_32_87336_117_22743") + assert state is not None + assert int(state.state) == 3 + assert state.name == "GDACS (32.87336, -117.22743)" + attributes = state.attributes + assert attributes[ATTR_STATUS] == "OK" + assert attributes[ATTR_CREATED] == 3 + assert attributes[ATTR_LAST_UPDATE].tzinfo == dt_util.UTC + assert attributes[ATTR_LAST_UPDATE_SUCCESSFUL].tzinfo == dt_util.UTC + assert attributes[ATTR_LAST_UPDATE] == attributes[ATTR_LAST_UPDATE_SUCCESSFUL] + assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "alerts" + assert attributes[ATTR_ICON] == "mdi:alert" + + # Simulate an update - two existing, one new entry, one outdated entry + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3] + async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + state = hass.states.get("sensor.gdacs_32_87336_117_22743") + attributes = state.attributes + assert attributes[ATTR_CREATED] == 1 + assert attributes[ATTR_UPDATED] == 2 + assert attributes[ATTR_REMOVED] == 1 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed_update.return_value = "OK_NO_DATA", None + async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, removes all entities + mock_feed_update.return_value = "ERROR", None + async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 1 + + state = hass.states.get("sensor.gdacs_32_87336_117_22743") + attributes = state.attributes + assert attributes[ATTR_REMOVED] == 3 From a3b3924e212695bb9998aada088721771ac022ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Feb 2020 02:37:35 -0800 Subject: [PATCH 126/378] Update link when IO in event loop (#31519) --- homeassistant/helpers/entity.py | 8 ++++++-- tests/helpers/test_entity.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fa649561e3d..250b81bb0fb 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -365,13 +365,17 @@ class Entity(ABC): if end - start > 0.4 and not self._slow_reported: self._slow_reported = True + url = "https://github.com/home-assistant/home-assistant/issues?q=is%3Aopen+is%3Aissue" + if self.platform: + url += f"+label%3A%22integration%3A+{self.platform.platform_name}%22" + _LOGGER.warning( "Updating state for %s (%s) took %.3f seconds. " - "Please report platform to the developers at " - "https://goo.gl/Nvioub", + "Please create a bug report at %s", self.entity_id, type(self), end - start, + url, ) # Overwrite properties that have been set in the config file. diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 749c11ff1a5..9977c99904a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -661,3 +661,22 @@ async def test_capability_attrs(hass): assert state is not None assert state.state == STATE_UNAVAILABLE assert state.attributes["always"] == "there" + + +async def test_warn_slow_write_state(hass, caplog): + """Check that we log a warning if reading properties takes too long.""" + mock_entity = entity.Entity() + mock_entity.hass = hass + mock_entity.entity_id = "comp_test.test_entity" + mock_entity.platform = MagicMock(platform_name="hue") + + with patch("homeassistant.helpers.entity.timer", side_effect=[0, 10]): + mock_entity.async_write_ha_state() + + assert ( + "Updating state for comp_test.test_entity " + "() " + "took 10.000 seconds. Please create a bug report at " + "https://github.com/home-assistant/home-assistant/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" + ) in caplog.text From 1cfd69d4848927fa5590c005af7da12d6cea9656 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 Feb 2020 12:54:46 +0100 Subject: [PATCH 127/378] Remove of liveboxplaytv integration (ADR0004) (#31525) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/liveboxplaytv/__init__.py | 1 - .../components/liveboxplaytv/manifest.json | 8 - .../components/liveboxplaytv/media_player.py | 273 ------------------ requirements_all.txt | 6 - 6 files changed, 290 deletions(-) delete mode 100644 homeassistant/components/liveboxplaytv/__init__.py delete mode 100644 homeassistant/components/liveboxplaytv/manifest.json delete mode 100644 homeassistant/components/liveboxplaytv/media_player.py diff --git a/.coveragerc b/.coveragerc index 8fe53ec8282..6e2ea5ba89b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -386,7 +386,6 @@ omit = homeassistant/components/linode/* homeassistant/components/linux_battery/sensor.py homeassistant/components/lirc/* - homeassistant/components/liveboxplaytv/media_player.py homeassistant/components/llamalab_automate/notify.py homeassistant/components/lockitron/lock.py homeassistant/components/logi_circle/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 47cc30f1117..4078a45f990 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -192,7 +192,6 @@ homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff -homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd diff --git a/homeassistant/components/liveboxplaytv/__init__.py b/homeassistant/components/liveboxplaytv/__init__.py deleted file mode 100644 index 384c0e4c34b..00000000000 --- a/homeassistant/components/liveboxplaytv/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The liveboxplaytv component.""" diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json deleted file mode 100644 index a05ff27ca90..00000000000 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "liveboxplaytv", - "name": "Orange Livebox Play TV", - "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", - "requirements": ["liveboxplaytv==2.0.3", "pyteleloisirs==3.6"], - "dependencies": [], - "codeowners": ["@pschmitt"] -} diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py deleted file mode 100644 index 66fb383d677..00000000000 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ /dev/null @@ -1,273 +0,0 @@ -"""Support for interface with an Orange Livebox Play TV appliance.""" -from datetime import timedelta -import logging - -from liveboxplaytv import LiveboxPlayTv -import pyteleloisirs -import requests -import voluptuous as vol - -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Livebox Play TV" -DEFAULT_PORT = 8080 - -SUPPORT_LIVEBOXPLAYTV = ( - SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_NEXT_TRACK - | SUPPORT_PAUSE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_SELECT_SOURCE - | SUPPORT_PLAY -) - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Orange Livebox Play TV platform.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) - - livebox_devices = [] - - try: - device = LiveboxPlayTvDevice(host, port, name) - livebox_devices.append(device) - except OSError: - _LOGGER.error( - "Failed to connect to Livebox Play TV at %s:%s. " - "Please check your configuration", - host, - port, - ) - async_add_entities(livebox_devices, True) - - -class LiveboxPlayTvDevice(MediaPlayerDevice): - """Representation of an Orange Livebox Play TV.""" - - def __init__(self, host, port, name): - """Initialize the Livebox Play TV device.""" - - self._client = LiveboxPlayTv(host, port) - # Assume that the appliance is not muted - self._muted = False - self._name = name - self._current_source = None - self._state = None - self._channel_list = {} - self._current_channel = None - self._current_program = None - self._media_duration = None - self._media_remaining_time = None - self._media_image_url = None - self._media_last_updated = None - - async def async_update(self): - """Retrieve the latest data.""" - - try: - self._state = self.refresh_state() - # Update channel list - self.refresh_channel_list() - # Update current channel - channel = self._client.channel - if channel is not None: - self._current_channel = channel - program = await self._client.async_get_current_program() - if program and self._current_program != program.get("name"): - self._current_program = program.get("name") - # Media progress info - self._media_duration = pyteleloisirs.get_program_duration(program) - rtime = pyteleloisirs.get_remaining_time(program) - if rtime != self._media_remaining_time: - self._media_remaining_time = rtime - self._media_last_updated = dt_util.utcnow() - # Set media image to current program if a thumbnail is - # available. Otherwise we'll use the channel's image. - img_size = 800 - prg_img_url = await self._client.async_get_current_program_image( - img_size - ) - if prg_img_url: - self._media_image_url = prg_img_url - else: - chan_img_url = self._client.get_current_channel_image(img_size) - self._media_image_url = chan_img_url - except requests.ConnectionError: - self._state = None - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def source(self): - """Return the current input source.""" - return self._current_channel - - @property - def source_list(self): - """List of available input sources.""" - # Sort channels by tvIndex - return [self._channel_list[c] for c in sorted(self._channel_list.keys())] - - @property - def media_content_type(self): - """Content type of current playing media.""" - # return self._client.media_type - return MEDIA_TYPE_CHANNEL - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._media_image_url - - @property - def media_title(self): - """Title of current playing media.""" - if self._current_channel: - if self._current_program: - return f"{self._current_channel}: {self._current_program}" - return self._current_channel - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self._media_duration - - @property - def media_position(self): - """Position of current playing media in seconds.""" - return self._media_remaining_time - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid. - - Returns value from homeassistant.util.dt.utcnow(). - """ - return self._media_last_updated - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_LIVEBOXPLAYTV - - def refresh_channel_list(self): - """Refresh the list of available channels.""" - new_channel_list = {} - # update channels - for channel in self._client.get_channels(): - new_channel_list[int(channel["index"])] = channel["name"] - self._channel_list = new_channel_list - - def refresh_state(self): - """Refresh the current media state.""" - state = self._client.media_state - if state == "PLAY": - return STATE_PLAYING - if state == "PAUSE": - return STATE_PAUSED - - return STATE_ON if self._client.is_on else STATE_OFF - - def turn_off(self): - """Turn off media player.""" - self._state = STATE_OFF - self._client.turn_off() - - def turn_on(self): - """Turn on the media player.""" - self._state = STATE_ON - self._client.turn_on() - - def volume_up(self): - """Volume up the media player.""" - self._client.volume_up() - - def volume_down(self): - """Volume down media player.""" - self._client.volume_down() - - def mute_volume(self, mute): - """Send mute command.""" - self._muted = mute - self._client.mute() - - def media_play_pause(self): - """Simulate play pause media player.""" - self._client.play_pause() - - def select_source(self, source): - """Select input source.""" - self._current_source = source - self._client.set_channel(source) - - def media_play(self): - """Send play command.""" - self._state = STATE_PLAYING - self._client.play() - - def media_pause(self): - """Send media pause command to media player.""" - self._state = STATE_PAUSED - self._client.pause() - - def media_next_track(self): - """Send next track command.""" - self._client.channel_up() - - def media_previous_track(self): - """Send the previous track command.""" - self._client.channel_down() diff --git a/requirements_all.txt b/requirements_all.txt index 1a6eb4905a0..8e9d8bb0301 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -801,9 +801,6 @@ limitlessled==1.1.3 # homeassistant.components.linode linode-api==4.1.9b1 -# homeassistant.components.liveboxplaytv -liveboxplaytv==2.0.3 - # homeassistant.components.lametric lmnotify==0.0.4 @@ -1535,9 +1532,6 @@ pysyncthru==0.5.0 # homeassistant.components.tautulli pytautulli==0.5.0 -# homeassistant.components.liveboxplaytv -pyteleloisirs==3.6 - # homeassistant.components.tfiac pytfiac==0.4 From 44e243039cdf342c0d6ac346634125408f6f990f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Feb 2020 03:55:11 -0800 Subject: [PATCH 128/378] Fix automation sun import (#31521) --- homeassistant/components/automation/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 528a314dd7b..1a73de885c0 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -5,7 +5,6 @@ from typing import Any, Awaitable, Callable, List, Optional, Set import voluptuous as vol -from homeassistant.components import sun from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -578,6 +577,6 @@ def _trigger_extract_entities(trigger_conf: dict) -> List[str]: return [trigger_conf[CONF_ZONE]] if trigger_conf[CONF_PLATFORM] == "sun": - return [sun.ENTITY_ID] + return ["sun.sun"] return [] From 4f2195101cde305c0c82d3d8d411fed9aec97581 Mon Sep 17 00:00:00 2001 From: P-Verbrugge <41943098+P-Verbrugge@users.noreply.github.com> Date: Thu, 6 Feb 2020 15:41:48 +0100 Subject: [PATCH 129/378] Updated the provider name to blockchain.com (#31534) * Updated the provider name to blockchain.com Blockchain.info moved from .info .com. Updated the name of the service to blockchain.com * Updated the provider name to blockchain.com Updated the provider name to blockchain.com --- homeassistant/components/blockchain/manifest.json | 2 +- homeassistant/components/blockchain/sensor.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index f57e91a9262..324abf792df 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -1,6 +1,6 @@ { "domain": "blockchain", - "name": "Blockchain.info", + "name": "Blockchain.com", "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": ["python-blockchain-api==0.0.2"], "dependencies": [], diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 6d17484bdd7..acf86957957 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -1,4 +1,4 @@ -"""Support for Blockchain.info sensors.""" +"""Support for Blockchain.com sensors.""" from datetime import timedelta import logging @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.com" CONF_ADDRESSES = "addresses" @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Blockchain.info sensors.""" + """Set up the Blockchain.com sensors.""" addresses = config.get(CONF_ADDRESSES) name = config.get(CONF_NAME) @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlockchainSensor(Entity): - """Representation of a Blockchain.info sensor.""" + """Representation of a Blockchain.com sensor.""" def __init__(self, name, addresses): """Initialize the sensor.""" From d3e99f13ddd04a6823478aa7ce1035ca823847f7 Mon Sep 17 00:00:00 2001 From: P-Verbrugge <41943098+P-Verbrugge@users.noreply.github.com> Date: Thu, 6 Feb 2020 15:43:52 +0100 Subject: [PATCH 130/378] Changed website name to blockchain.com (#31528) The name and domain of blockchain.info has been changed to blockchain.com. Updated the names in de script. --- homeassistant/components/bitcoin/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index bc8394d51a5..6a8651be6dc 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,4 +1,4 @@ -"""Bitcoin information service that uses blockchain.info.""" +"""Bitcoin information service that uses blockchain.com.""" from datetime import timedelta import logging @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.com" DEFAULT_CURRENCY = "USD" @@ -168,7 +168,7 @@ class BitcoinData: self.ticker = None def update(self): - """Get the latest data from blockchain.info.""" + """Get the latest data from blockchain.com.""" self.stats = statistics.get() self.ticker = exchangerates.get_ticker() From 24e9a638d5f57432bdb1e5ad85287a5bcbe9e11a Mon Sep 17 00:00:00 2001 From: Robert Chmielowiec Date: Thu, 6 Feb 2020 15:53:16 +0100 Subject: [PATCH 131/378] Fix migrating huawei_lte entry without recipient (#31522) --- homeassistant/components/huawei_lte/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 1b8cb658c28..1d54f972907 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -510,7 +510,7 @@ async def async_migrate_entry(hass: HomeAssistantType, config_entry: ConfigEntry """Migrate config entry to new version.""" if config_entry.version == 1: options = config_entry.options - recipient = options[CONF_RECIPIENT] + recipient = options.get(CONF_RECIPIENT) if isinstance(recipient, str): options[CONF_RECIPIENT] = [x.strip() for x in recipient.split(",")] config_entry.version = 2 From 7233048feafa425f86c7fd07b3ece8adbf6e7acc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 Feb 2020 17:00:27 +0100 Subject: [PATCH 132/378] Limit OAuth scopes for Netatmo and Home Assistant Cloud (#31538) * Limit OAuth scopes for Netatmo and Home Assistant Cloud * Fix tests by making order of scopes predictable --- .../components/netatmo/config_flow.py | 34 +++++++++---------- tests/components/netatmo/test_config_flow.py | 8 ++--- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 8f59382dd46..dce87fb7931 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -25,24 +25,22 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return { - "scope": ( - " ".join( - [ - "read_station", - "read_camera", - "access_camera", - "write_camera", - "read_presence", - "access_presence", - "read_homecoach", - "read_smokedetector", - "read_thermostat", - "write_thermostat", - ] - ) - ) - } + scopes = [ + "read_camera", + "read_homecoach", + "read_presence", + "read_smokedetector", + "read_station", + "read_thermostat", + "write_camera", + "write_thermostat", + ] + + if self.flow_impl.name != "Home Assistant Cloud": + scopes.extend(["access_camera", "access_presence"]) + scopes.sort() + + return {"scope": " ".join(scopes)} async def async_step_user(self, user_input=None): """Handle a flow start.""" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 24aac6dc878..d76578d277c 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -54,15 +54,15 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): scope = "+".join( [ - "read_station", - "read_camera", "access_camera", - "write_camera", - "read_presence", "access_presence", + "read_camera", "read_homecoach", + "read_presence", "read_smokedetector", + "read_station", "read_thermostat", + "write_camera", "write_thermostat", ] ) From 24c382d689c6980bb702763c7a4901eb93cc58f2 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Thu, 6 Feb 2020 16:52:46 +0000 Subject: [PATCH 133/378] Only normalise Garmin connect data to minutes if the value is not None (#31526) Otherwise this causes additional TypeError messages to be logged for division of None. --- homeassistant/components/garmin_connect/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 51ec421e02b..737d53b2109 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -165,9 +165,9 @@ class GarminConnectSensor(Entity): return data = self._data.data - if "Duration" in self._type: + if "Duration" in self._type and data[self._type]: self._state = data[self._type] // 60 - elif "Seconds" in self._type: + elif "Seconds" in self._type and data[self._type]: self._state = data[self._type] // 60 else: self._state = data[self._type] From 9e4904cb218544dd6d5e03662a4558845f316415 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 6 Feb 2020 17:53:42 +0100 Subject: [PATCH 134/378] Fix iCloud determine_interval: add default interval to max_interval (#31533) --- homeassistant/components/icloud/account.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index af7963d8dc1..5d681539668 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -26,6 +26,7 @@ from .const import ( DEVICE_DISPLAY_NAME, DEVICE_ID, DEVICE_LOCATION, + DEVICE_LOCATION_HORIZONTAL_ACCURACY, DEVICE_LOCATION_LATITUDE, DEVICE_LOCATION_LONGITUDE, DEVICE_LOST_MODE_CAPABLE, @@ -175,8 +176,9 @@ class IcloudAccount: def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" - intervals = {} + intervals = {"default": self._max_interval} for device in self._devices.values(): + # Max interval if no location if device.location is None: continue @@ -186,10 +188,11 @@ class IcloudAccount: self.hass, device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], + device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY], ).result() + # Max interval if in zone if current_zone is not None: - intervals[device.name] = self._max_interval continue zones = ( @@ -209,6 +212,7 @@ class IcloudAccount: ) distances.append(round(zone_distance / 1000, 1)) + # Max interval if no zone if not distances: continue mindistance = min(distances) From d407b8e2152be6f1170ec86fc76f089c11bc3c38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Feb 2020 09:26:51 -0800 Subject: [PATCH 135/378] Deprecate old netatmo keys (#31544) --- homeassistant/components/netatmo/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index ace12d3838c..bd79f597b5b 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -5,7 +5,12 @@ import logging import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DISCOVERY, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv @@ -14,12 +19,19 @@ from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN _LOGGER = logging.getLogger(__name__) +CONF_SECRET_KEY = "secret_key" +CONF_WEBHOOKS = "webhooks" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, + cv.deprecated(CONF_SECRET_KEY): cv.match_all, + cv.deprecated(CONF_USERNAME): cv.match_all, + cv.deprecated(CONF_WEBHOOKS): cv.match_all, + cv.deprecated(CONF_DISCOVERY): cv.match_all, } ) }, From 0d474e1183acb73e62965d66838d0f6bd45c7617 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Feb 2020 09:29:29 -0800 Subject: [PATCH 136/378] Update the update coordinator API to make it easier to use (#31471) * Update the update coordinator API to make it easier to use * failed_last_update -> last_update_success --- homeassistant/components/hue/light.py | 24 +++++++++++-------- homeassistant/components/hue/sensor_base.py | 12 ++++++---- homeassistant/components/updater/__init__.py | 6 ++++- .../components/updater/binary_sensor.py | 2 +- homeassistant/helpers/debounce.py | 1 + homeassistant/helpers/update_coordinator.py | 23 +++++++++++------- tests/components/hue/test_light.py | 12 +++++----- tests/helpers/test_debounce.py | 12 ++++++++-- tests/helpers/test_update_coordinator.py | 18 ++++++++------ 9 files changed, 69 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 7ed2dcc84f2..2e27bf65c98 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -79,17 +79,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): light_coordinator = DataUpdateCoordinator( hass, _LOGGER, - "light", - partial(async_safe_fetch, bridge, bridge.api.lights.update), - SCAN_INTERVAL, - Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + name="light", + update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), ) # First do a refresh to see if we can reach the hub. # Otherwise we will declare not ready. await light_coordinator.async_refresh() - if light_coordinator.failed_last_update: + if not light_coordinator.last_update_success: raise PlatformNotReady update_lights = partial( @@ -122,10 +124,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): group_coordinator = DataUpdateCoordinator( hass, _LOGGER, - "group", - partial(async_safe_fetch, bridge, bridge.api.groups.update), - SCAN_INTERVAL, - Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + name="group", + update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), ) update_groups = partial( @@ -277,7 +281,7 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return not self.coordinator.failed_last_update and ( + return self.coordinator.last_update_success and ( self.is_group or self.bridge.allow_unreachable or self.light.state["reachable"] diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index f57b0f98d30..1e518c05ee5 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -42,10 +42,12 @@ class SensorManager: self.coordinator = DataUpdateCoordinator( bridge.hass, _LOGGER, - "sensor", - self.async_update_data, - self.SCAN_INTERVAL, - debounce.Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + name="sensor", + update_method=self.async_update_data, + update_interval=self.SCAN_INTERVAL, + request_refresh_debouncer=debounce.Debouncer( + bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), ) async def async_update_data(self): @@ -183,7 +185,7 @@ class GenericHueSensor(entity.Entity): @property def available(self): """Return if sensor is available.""" - return not self.bridge.sensor_manager.coordinator.failed_last_update and ( + return self.bridge.sensor_manager.coordinator.last_update_success and ( self.bridge.allow_unreachable or self.sensor.config["reachable"] ) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 0f388964bb0..0a2c6697f69 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -123,7 +123,11 @@ async def async_setup(hass, config): return Updater(update_available, newest, release_notes) coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator( - hass, _LOGGER, "Home Assistant update", check_new_version, timedelta(days=1) + hass, + _LOGGER, + name="Home Assistant update", + update_method=check_new_version, + update_interval=timedelta(days=1), ) await coordinator.async_refresh() diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 60f5cfedf6e..7abab616d5c 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -38,7 +38,7 @@ class UpdaterBinary(BinarySensorDevice): @property def available(self) -> bool: """Return True if entity is available.""" - return not self.coordinator.failed_last_update + return self.coordinator.last_update_success @property def should_poll(self) -> bool: diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 5bacbdb7d11..bbaf6dacfeb 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -13,6 +13,7 @@ class Debouncer: self, hass: HomeAssistant, logger: Logger, + *, cooldown: float, immediate: bool, function: Optional[Callable[..., Awaitable[Any]]] = None, diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index a882e2880b1..5f0490b6ea2 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -26,6 +26,7 @@ class DataUpdateCoordinator: self, hass: HomeAssistant, logger: logging.Logger, + *, name: str, update_method: Callable[[], Awaitable], update_interval: timedelta, @@ -43,16 +44,20 @@ class DataUpdateCoordinator: self._listeners: List[CALLBACK_TYPE] = [] self._unsub_refresh: Optional[CALLBACK_TYPE] = None self._request_refresh_task: Optional[asyncio.TimerHandle] = None - self.failed_last_update = False + self.last_update_success = True + if request_refresh_debouncer is None: request_refresh_debouncer = Debouncer( hass, logger, - REQUEST_REFRESH_DEFAULT_COOLDOWN, - REQUEST_REFRESH_DEFAULT_IMMEDIATE, + cooldown=REQUEST_REFRESH_DEFAULT_COOLDOWN, + immediate=REQUEST_REFRESH_DEFAULT_IMMEDIATE, + function=self.async_refresh, ) + else: + request_refresh_debouncer.function = self.async_refresh + self._debounced_refresh = request_refresh_debouncer - request_refresh_debouncer.function = self.async_refresh @callback def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None: @@ -110,19 +115,19 @@ class DataUpdateCoordinator: self.data = await self.update_method() except UpdateFailed as err: - if not self.failed_last_update: + if self.last_update_success: self.logger.error("Error fetching %s data: %s", self.name, err) - self.failed_last_update = True + self.last_update_success = False except Exception as err: # pylint: disable=broad-except - self.failed_last_update = True + self.last_update_success = False self.logger.exception( "Unexpected error fetching %s data: %s", self.name, err ) else: - if self.failed_last_update: - self.failed_last_update = False + if not self.last_update_success: + self.last_update_success = True self.logger.info("Fetching %s data recovered") finally: diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index df3fe5f8998..d57c15bfa36 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -703,7 +703,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - coordinator=Mock(failed_last_update=False), + coordinator=Mock(last_update_success=True), bridge=Mock(allow_unreachable=False), is_group=False, ) @@ -717,7 +717,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - coordinator=Mock(failed_last_update=False), + coordinator=Mock(last_update_success=True), bridge=Mock(allow_unreachable=True), is_group=False, ) @@ -731,7 +731,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - coordinator=Mock(failed_last_update=False), + coordinator=Mock(last_update_success=True), bridge=Mock(allow_unreachable=False), is_group=True, ) @@ -748,7 +748,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - coordinator=Mock(failed_last_update=False), + coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, ) @@ -762,7 +762,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - coordinator=Mock(failed_last_update=False), + coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, ) @@ -776,7 +776,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - coordinator=Mock(failed_last_update=False), + coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, ) diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index d7629a393a9..4972fbbc018 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -8,7 +8,11 @@ async def test_immediate_works(hass): """Test immediate works.""" calls = [] debouncer = debounce.Debouncer( - hass, None, 0.01, True, CoroutineMock(side_effect=lambda: calls.append(None)) + hass, + None, + cooldown=0.01, + immediate=True, + function=CoroutineMock(side_effect=lambda: calls.append(None)), ) await debouncer.async_call() @@ -37,7 +41,11 @@ async def test_not_immediate_works(hass): """Test immediate works.""" calls = [] debouncer = debounce.Debouncer( - hass, None, 0.01, False, CoroutineMock(side_effect=lambda: calls.append(None)) + hass, + None, + cooldown=0.01, + immediate=False, + function=CoroutineMock(side_effect=lambda: calls.append(None)), ) await debouncer.async_call() diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 8c792506833..04fd180b60d 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -23,7 +23,11 @@ def crd(hass): return len(calls) crd = update_coordinator.DataUpdateCoordinator( - hass, LOGGER, "test", refresh, timedelta(seconds=10), + hass, + LOGGER, + name="test", + update_method=refresh, + update_interval=timedelta(seconds=10), ) return crd @@ -33,7 +37,7 @@ async def test_async_refresh(crd): assert crd.data is None await crd.async_refresh() assert crd.data == 1 - assert crd.failed_last_update is False + assert crd.last_update_success is True updates = [] @@ -58,12 +62,12 @@ async def test_request_refresh(crd): assert crd.data is None await crd.async_request_refresh() assert crd.data == 1 - assert crd.failed_last_update is False + assert crd.last_update_success is True # Second time we hit the debonuce await crd.async_request_refresh() assert crd.data == 1 - assert crd.failed_last_update is False + assert crd.last_update_success is True async def test_refresh_fail(crd, caplog): @@ -73,7 +77,7 @@ async def test_refresh_fail(crd, caplog): await crd.async_refresh() assert crd.data is None - assert crd.failed_last_update is True + assert crd.last_update_success is False assert "Error fetching test data" in caplog.text crd.update_method = CoroutineMock(return_value=1) @@ -81,7 +85,7 @@ async def test_refresh_fail(crd, caplog): await crd.async_refresh() assert crd.data == 1 - assert crd.failed_last_update is False + assert crd.last_update_success is True crd.update_method = CoroutineMock(side_effect=ValueError) caplog.clear() @@ -89,7 +93,7 @@ async def test_refresh_fail(crd, caplog): await crd.async_refresh() assert crd.data == 1 # value from previous fetch - assert crd.failed_last_update is True + assert crd.last_update_success is False assert "Unexpected error fetching test data" in caplog.text From eee1ca921134f75450a79041b3221b78409ae003 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Thu, 6 Feb 2020 19:00:54 +0100 Subject: [PATCH 137/378] update aiopylgtv to 0.3.3 (#31545) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index e55867432cc..acdee1d9ca9 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -2,7 +2,7 @@ "domain": "webostv", "name": "LG webOS Smart TV", "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiopylgtv==0.3.2"], + "requirements": ["aiopylgtv==0.3.3"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8e9d8bb0301..92dd07500b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -193,7 +193,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.3.2 +aiopylgtv==0.3.3 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1d9d07ded1..b4aecce3947 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.3.2 +aiopylgtv==0.3.3 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From 08a74ff6860552fab767856fad0967b51a8e86de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 6 Feb 2020 20:54:01 +0200 Subject: [PATCH 138/378] Use min and m as units in Garmin Connect for consistency and correctness (#31543) --- .../components/garmin_connect/const.py | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 57cd35e667f..e38bd72c1ee 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -27,7 +27,7 @@ GARMIN_ENTITY_LIST = { False, ], "netCalorieGoal": ["Net Calorie Goal", "cal", "mdi:food", None, False], - "totalDistanceMeters": ["Total Distance Mtr", "mtr", "mdi:walk", None, True], + "totalDistanceMeters": ["Total Distance Mtr", "m", "mdi:walk", None, True], "wellnessStartTimeLocal": [ "Wellness Start Time", "", @@ -43,7 +43,7 @@ GARMIN_ENTITY_LIST = { False, ], "wellnessDescription": ["Wellness Description", "", "mdi:clock", None, False], - "wellnessDistanceMeters": ["Wellness Distance Mtr", "mtr", "mdi:walk", None, False], + "wellnessDistanceMeters": ["Wellness Distance Mtr", "m", "mdi:walk", None, False], "wellnessActiveKilocalories": [ "Wellness Active KiloCalories", "kcal", @@ -52,16 +52,16 @@ GARMIN_ENTITY_LIST = { False, ], "wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food", None, False], - "highlyActiveSeconds": ["Highly Active Time", "minutes", "mdi:fire", None, False], - "activeSeconds": ["Active Time", "minutes", "mdi:fire", None, True], - "sedentarySeconds": ["Sedentary Time", "minutes", "mdi:seat", None, True], - "sleepingSeconds": ["Sleeping Time", "minutes", "mdi:sleep", None, True], - "measurableAwakeDuration": ["Awake Duration", "minutes", "mdi:sleep", None, True], - "measurableAsleepDuration": ["Sleep Duration", "minutes", "mdi:sleep", None, True], - "floorsAscendedInMeters": ["Floors Ascended Mtr", "mtr", "mdi:stairs", None, False], + "highlyActiveSeconds": ["Highly Active Time", "min", "mdi:fire", None, False], + "activeSeconds": ["Active Time", "min", "mdi:fire", None, True], + "sedentarySeconds": ["Sedentary Time", "min", "mdi:seat", None, True], + "sleepingSeconds": ["Sleeping Time", "min", "mdi:sleep", None, True], + "measurableAwakeDuration": ["Awake Duration", "min", "mdi:sleep", None, True], + "measurableAsleepDuration": ["Sleep Duration", "min", "mdi:sleep", None, True], + "floorsAscendedInMeters": ["Floors Ascended Mtr", "m", "mdi:stairs", None, False], "floorsDescendedInMeters": [ "Floors Descended Mtr", - "mtr", + "m", "mdi:stairs", None, False, @@ -97,52 +97,46 @@ GARMIN_ENTITY_LIST = { "averageStressLevel": ["Avg Stress Level", "", "mdi:flash-alert", None, True], "maxStressLevel": ["Max Stress Level", "", "mdi:flash-alert", None, True], "stressQualifier": ["Stress Qualifier", "", "mdi:flash-alert", None, False], - "stressDuration": ["Stress Duration", "minutes", "mdi:flash-alert", None, False], + "stressDuration": ["Stress Duration", "min", "mdi:flash-alert", None, False], "restStressDuration": [ "Rest Stress Duration", - "minutes", + "min", "mdi:flash-alert", None, True, ], "activityStressDuration": [ "Activity Stress Duration", - "minutes", + "min", "mdi:flash-alert", None, True, ], "uncategorizedStressDuration": [ "Uncat. Stress Duration", - "minutes", + "min", "mdi:flash-alert", None, True, ], "totalStressDuration": [ "Total Stress Duration", - "minutes", - "mdi:flash-alert", - None, - True, - ], - "lowStressDuration": [ - "Low Stress Duration", - "minutes", + "min", "mdi:flash-alert", None, True, ], + "lowStressDuration": ["Low Stress Duration", "min", "mdi:flash-alert", None, True], "mediumStressDuration": [ "Medium Stress Duration", - "minutes", + "min", "mdi:flash-alert", None, True, ], "highStressDuration": [ "High Stress Duration", - "minutes", + "min", "mdi:flash-alert", None, True, @@ -192,19 +186,19 @@ GARMIN_ENTITY_LIST = { ], "moderateIntensityMinutes": [ "Moderate Intensity", - "minutes", + "min", "mdi:flash-alert", None, False, ], "vigorousIntensityMinutes": [ "Vigorous Intensity", - "minutes", + "min", "mdi:run-fast", None, False, ], - "intensityMinutesGoal": ["Intensity Goal", "minutes", "mdi:run-fast", None, False], + "intensityMinutesGoal": ["Intensity Goal", "min", "mdi:run-fast", None, False], "bodyBatteryChargedValue": [ "Body Battery Charged", "%", From 00c6f3cb854b6a7015fd9474d9af1bc46f819b3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Feb 2020 10:55:12 -0800 Subject: [PATCH 139/378] Guard for reloading with no zone config (#31547) --- homeassistant/components/zone/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 91a1338b671..33ac15853f4 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -228,7 +228,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: conf = await component.async_prepare_reload(skip_reset=True) if conf is None: return - await yaml_collection.async_load(conf[DOMAIN]) + await yaml_collection.async_load(conf.get(DOMAIN, [])) service.async_register_admin_service( hass, From 9e87a662d58120a17ffaab23cd3119687b1af958 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Feb 2020 11:39:44 -0800 Subject: [PATCH 140/378] Update MQTT service description --- homeassistant/components/mqtt/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 77b3e3b27a1..2af3c22fe50 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -30,7 +30,7 @@ dump: fields: topic: description: topic to listen to - example: "openzwave/#" + example: "OpenZWave/#" duration: description: how long we should listen for messages in seconds example: 5 From d1e7ade6dbd3fbbba91e6c0edde55cdcbdfeac21 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 6 Feb 2020 14:44:48 -0600 Subject: [PATCH 141/378] Make amcrest integration more robust (#30843) - Bump amcrest package to 1.5.6. Includes networking improvements, no longer communicates during Http.__init__(), and allows running snapshot command without using stream mode. - Handle login errors better, and not just at startup. - Increase network connect & read timeout to 6.05 seconds. - Increase network read timeout to 20 seconds for snapshot command. - Run snapshot command in separate task, that cannot be cancelled, to eliminate possibility of two snapshot commands running simultaneously (since AmcrestCam.async_camera_image can be cancelled.) Also makes sure any exceptions from the command are caught properly. --- homeassistant/components/amcrest/__init__.py | 62 +++++++++++++------ homeassistant/components/amcrest/camera.py | 60 ++++++++++++++---- homeassistant/components/amcrest/const.py | 3 + .../components/amcrest/manifest.json | 2 +- requirements_all.txt | 2 +- 5 files changed, 95 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index d1e1aafa6f3..5578d350e22 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -35,7 +35,15 @@ from homeassistant.helpers.service import async_extract_entity_ids from .binary_sensor import BINARY_SENSORS from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST -from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE +from .const import ( + CAMERAS, + COMM_RETRIES, + COMM_TIMEOUT, + DATA_AMCREST, + DEVICES, + DOMAIN, + SERVICE_UPDATE, +) from .helpers import service_signal from .sensor import SENSORS @@ -111,38 +119,56 @@ class AmcrestChecker(Http): self._wrap_name = name self._wrap_errors = 0 self._wrap_lock = threading.Lock() + self._wrap_login_err = False self._unsub_recheck = None super().__init__( - host, port, user, password, retries_connection=1, timeout_protocol=3.05 + host, + port, + user, + password, + retries_connection=COMM_RETRIES, + timeout_protocol=COMM_TIMEOUT, ) @property def available(self): """Return if camera's API is responding.""" - return self._wrap_errors <= MAX_ERRORS + return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err + + def _start_recovery(self): + dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) + self._unsub_recheck = track_time_interval( + self._hass, self._wrap_test_online, RECHECK_INTERVAL + ) def command(self, cmd, retries=None, timeout_cmd=None, stream=False): """amcrest.Http.command wrapper to catch errors.""" try: ret = super().command(cmd, retries, timeout_cmd, stream) + except LoginError as ex: + with self._wrap_lock: + was_online = self.available + was_login_err = self._wrap_login_err + self._wrap_login_err = True + if not was_login_err: + _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) + if was_online: + self._start_recovery() + raise except AmcrestError: with self._wrap_lock: was_online = self.available - self._wrap_errors += 1 - _LOGGER.debug("%s camera errs: %i", self._wrap_name, self._wrap_errors) + errs = self._wrap_errors = self._wrap_errors + 1 offline = not self.available - if offline and was_online: + _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) + if was_online and offline: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - dispatcher_send( - self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) - ) - self._unsub_recheck = track_time_interval( - self._hass, self._wrap_test_online, RECHECK_INTERVAL - ) + self._start_recovery() raise with self._wrap_lock: was_offline = not self.available self._wrap_errors = 0 + self._wrap_login_err = False if was_offline: self._unsub_recheck() self._unsub_recheck = None @@ -152,6 +178,7 @@ class AmcrestChecker(Http): def _wrap_test_online(self, now): """Test if camera is back online.""" + _LOGGER.debug("Testing if %s back online", self._wrap_name) try: self.current_time except AmcrestError: @@ -167,14 +194,9 @@ def setup(hass, config): username = device[CONF_USERNAME] password = device[CONF_PASSWORD] - try: - api = AmcrestChecker( - hass, name, device[CONF_HOST], device[CONF_PORT], username, password - ) - - except LoginError as ex: - _LOGGER.error("Login error for %s camera: %s", name, ex) - continue + api = AmcrestChecker( + hass, name, device[CONF_HOST], device[CONF_PORT], username, password + ) ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS] resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 8ff6403c566..0e64d4fefc9 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -1,11 +1,11 @@ """Support for Amcrest IP cameras.""" import asyncio from datetime import timedelta +from functools import partial import logging from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg -from urllib3.exceptions import HTTPError import voluptuous as vol from homeassistant.components.camera import ( @@ -26,9 +26,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CAMERA_WEB_SESSION_TIMEOUT, CAMERAS, + COMM_TIMEOUT, DATA_AMCREST, DEVICES, SERVICE_UPDATE, + SNAPSHOT_TIMEOUT, ) from .helpers import log_update_error, service_signal @@ -90,6 +92,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True) +class CannotSnapshot(Exception): + """Conditions are not valid for taking a snapshot.""" + + class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" @@ -112,12 +118,11 @@ class AmcrestCam(Camera): self._motion_recording_enabled = None self._color_bw = None self._rtsp_url = None - self._snapshot_lock = asyncio.Lock() + self._snapshot_task = None self._unsub_dispatcher = [] self._update_succeeded = False - async def async_camera_image(self): - """Return a still image response from the camera.""" + def _check_snapshot_ok(self): available = self.available if not available or not self.is_on: _LOGGER.warning( @@ -125,15 +130,46 @@ class AmcrestCam(Camera): self.name, "offline" if not available else "off", ) + raise CannotSnapshot + + async def _async_get_image(self): + try: + # Send the request to snap a picture and return raw jpg data + # Snapshot command needs a much longer read timeout than other commands. + return await self.hass.async_add_executor_job( + partial( + self._api.snapshot, + timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT), + stream=False, + ) + ) + except AmcrestError as error: + log_update_error(_LOGGER, "get image from", self.name, "camera", error) + return None + finally: + self._snapshot_task = None + + async def async_camera_image(self): + """Return a still image response from the camera.""" + _LOGGER.debug("Take snapshot from %s", self._name) + try: + # Amcrest cameras only support one snapshot command at a time. + # Hence need to wait if a previous snapshot has not yet finished. + # Also need to check that camera is online and turned on before each wait + # and before initiating shapshot. + while self._snapshot_task: + self._check_snapshot_ok() + _LOGGER.debug("Waiting for previous snapshot from %s ...", self._name) + await self._snapshot_task + self._check_snapshot_ok() + # Run snapshot command in separate Task that can't be cancelled so + # 1) it's not possible to send another snapshot command while camera is + # still working on a previous one, and + # 2) someone will be around to catch any exceptions. + self._snapshot_task = self.hass.async_create_task(self._async_get_image()) + return await asyncio.shield(self._snapshot_task) + except CannotSnapshot: return None - async with self._snapshot_lock: - try: - # Send the request to snap a picture and return raw jpg data - response = await self.hass.async_add_executor_job(self._api.snapshot) - return response.data - except (AmcrestError, HTTPError) as error: - log_update_error(_LOGGER, "get image from", self.name, "camera", error) - return None async def handle_async_mjpeg_stream(self, request): """Return an MJPEG stream.""" diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 98d613634b5..38ff8a8894e 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -6,6 +6,9 @@ DEVICES = "devices" BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 CAMERA_WEB_SESSION_TIMEOUT = 10 +COMM_RETRIES = 1 +COMM_TIMEOUT = 6.05 SENSOR_SCAN_INTERVAL_SECS = 10 +SNAPSHOT_TIMEOUT = 20 SERVICE_UPDATE = "update" diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 8b2d72effa6..38e19e4ec26 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.5.3"], + "requirements": ["amcrest==1.5.6"], "dependencies": ["ffmpeg"], "codeowners": ["@pnbruckner"] } diff --git a/requirements_all.txt b/requirements_all.txt index 92dd07500b2..be6b266e9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ alpha_vantage==2.1.3 ambiclimate==0.2.1 # homeassistant.components.amcrest -amcrest==1.5.3 +amcrest==1.5.6 # homeassistant.components.androidtv androidtv==0.0.39 From 0e68ace3dddb0dc73f9c439c945f747174ff4fd3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 6 Feb 2020 16:34:28 -0500 Subject: [PATCH 142/378] Bump ZHA dependencies. (#31555) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f7f70db590a..c56544d1784 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,10 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.13.1", + "bellows-homeassistant==0.13.2", "zha-quirks==0.0.32", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.13.0", + "zigpy-homeassistant==0.13.1", "zigpy-xbee-homeassistant==0.9.0", "zigpy-zigate==0.5.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index be6b266e9d2..04a67822be2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -302,7 +302,7 @@ beautifulsoup4==4.8.2 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.13.1 +bellows-homeassistant==0.13.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -2127,7 +2127,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.13.0 +zigpy-homeassistant==0.13.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4aecce3947..d3bf97c15ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.13.1 +bellows-homeassistant==0.13.2 # homeassistant.components.bom bomradarloop==0.1.3 @@ -714,7 +714,7 @@ zha-quirks==0.0.32 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.13.0 +zigpy-homeassistant==0.13.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.9.0 From 3a3328dc13080a3540f17c6cc4fcf27dde283597 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 6 Feb 2020 16:26:34 -0600 Subject: [PATCH 143/378] Skip updates when Plex client viewing photos (#31556) --- homeassistant/components/plex/sensor.py | 3 +++ homeassistant/components/plex/server.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 2aed57946eb..4fe6ed444ef 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -101,6 +101,9 @@ class PlexSensor(Entity): _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) now_playing = [] for sess in self.sessions: + if sess.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", sess) + continue user = sess.usernames[0] device = sess.players[0].title now_playing_user = f"{user} - {device}" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 46602cf6552..ab5d79ff81c 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -132,6 +132,9 @@ class PlexServer: _LOGGER.debug("New device: %s", device.machineIdentifier) for session in sessions: + if session.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", session) + continue for player in session.players: self._known_idle.discard(player.machineIdentifier) available_clients.setdefault( From d093b5f5e8adb727708c98c64de5cc8cd13d76cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Feb 2020 01:02:00 +0100 Subject: [PATCH 144/378] Bump adguardhome to 0.4.1 (#31565) --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index bdfec1f254b..c77e0b3254d 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -3,7 +3,7 @@ "name": "AdGuard Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": ["adguardhome==0.4.0"], + "requirements": ["adguardhome==0.4.1"], "dependencies": [], "codeowners": ["@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 04a67822be2..a5f4ec023b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -117,7 +117,7 @@ adafruit-circuitpython-mcp230xx==1.1.2 adb-shell==0.1.1 # homeassistant.components.adguard -adguardhome==0.4.0 +adguardhome==0.4.1 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3bf97c15ea..0be5b9c88c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -32,7 +32,7 @@ abodepy==0.17.0 adb-shell==0.1.1 # homeassistant.components.adguard -adguardhome==0.4.0 +adguardhome==0.4.1 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 From 274cf232692396a6f0359a45d949ff94b681af52 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 7 Feb 2020 00:31:50 +0000 Subject: [PATCH 145/378] [ci skip] Translation update --- .../components/abode/.translations/sv.json | 22 +++++ .../components/adguard/.translations/sv.json | 2 + .../components/airly/.translations/sv.json | 25 +++++ .../alarm_control_panel/.translations/sv.json | 18 ++++ .../components/almond/.translations/sv.json | 12 ++- .../arcam_fmj/.translations/sv.json | 5 + .../components/axis/.translations/sv.json | 4 +- .../binary_sensor/.translations/sv.json | 94 +++++++++++++++++++ .../components/brother/.translations/fr.json | 5 +- .../components/brother/.translations/sv.json | 25 ++++- .../cert_expiry/.translations/sv.json | 26 +++++ .../components/climate/.translations/sv.json | 17 ++++ .../coolmaster/.translations/sv.json | 23 +++++ .../components/cover/.translations/sv.json | 20 ++++ .../components/deconz/.translations/sv.json | 71 ++++++++++++++ .../components/demo/.translations/sv.json | 5 + .../device_tracker/.translations/sv.json | 8 ++ .../components/ecobee/.translations/sv.json | 18 +++- .../components/elgato/.translations/sv.json | 27 ++++++ .../components/fan/.translations/sv.json | 16 ++++ .../garmin_connect/.translations/fr.json | 24 +++++ .../components/gdacs/.translations/ca.json | 16 ++++ .../components/gdacs/.translations/da.json | 16 ++++ .../components/gdacs/.translations/de.json | 16 ++++ .../components/gdacs/.translations/it.json | 16 ++++ .../components/gdacs/.translations/ko.json | 16 ++++ .../components/gdacs/.translations/ru.json | 16 ++++ .../components/gdacs/.translations/sv.json | 16 ++++ .../gdacs/.translations/zh-Hant.json | 16 ++++ .../geonetnz_quakes/.translations/ko.json | 2 +- .../geonetnz_quakes/.translations/sv.json | 17 ++++ .../geonetnz_volcano/.translations/ko.json | 2 +- .../geonetnz_volcano/.translations/sv.json | 16 ++++ .../components/gios/.translations/sv.json | 23 +++++ .../components/glances/.translations/sv.json | 37 ++++++++ .../hisense_aehw4a1/.translations/sv.json | 15 +++ .../huawei_lte/.translations/sv.json | 38 +++++++- .../iaqualink/.translations/sv.json | 21 +++++ .../components/icloud/.translations/sv.json | 34 ++++++- .../components/izone/.translations/sv.json | 15 +++ .../components/life360/.translations/sv.json | 1 + .../components/light/.translations/sv.json | 17 ++++ .../components/linky/.translations/sv.json | 19 +++- .../components/local_ip/.translations/sv.json | 16 ++++ .../components/lock/.translations/sv.json | 17 ++++ .../lutron_caseta/.translations/sv.json | 5 + .../components/met/.translations/sv.json | 2 +- .../meteo_france/.translations/ca.json | 5 + .../meteo_france/.translations/de.json | 1 + .../meteo_france/.translations/fr.json | 18 ++++ .../meteo_france/.translations/it.json | 18 ++++ .../components/mikrotik/.translations/fr.json | 36 +++++++ .../components/mikrotik/.translations/sv.json | 27 ++++++ .../components/neato/.translations/sv.json | 27 ++++++ .../opentherm_gw/.translations/sv.json | 34 +++++++ .../components/plex/.translations/sv.json | 56 ++++++++++- .../components/ring/.translations/sv.json | 1 + .../samsungtv/.translations/ca.json | 2 + .../samsungtv/.translations/da.json | 2 +- .../samsungtv/.translations/de.json | 2 + .../samsungtv/.translations/fr.json | 2 + .../samsungtv/.translations/it.json | 8 +- .../samsungtv/.translations/ko.json | 2 +- .../components/sensor/.translations/sv.json | 26 +++++ .../components/sentry/.translations/sv.json | 18 ++++ .../solaredge/.translations/sv.json | 21 +++++ .../components/solarlog/.translations/sv.json | 21 +++++ .../components/soma/.translations/sv.json | 25 +++++ .../components/somfy/.translations/sv.json | 5 + .../components/starline/.translations/sv.json | 35 ++++++- .../components/switch/.translations/sv.json | 19 ++++ .../components/tesla/.translations/sv.json | 30 ++++++ .../components/traccar/.translations/sv.json | 18 ++++ .../transmission/.translations/sv.json | 12 ++- .../twentemilieu/.translations/sv.json | 23 +++++ .../components/unifi/.translations/sv.json | 15 ++- .../components/vacuum/.translations/sv.json | 16 ++++ .../components/velbus/.translations/sv.json | 21 +++++ .../components/vesync/.translations/sv.json | 20 ++++ .../components/vizio/.translations/ca.json | 1 + .../components/vizio/.translations/fr.json | 1 + .../components/vizio/.translations/it.json | 1 + .../components/vizio/.translations/sv.json | 25 +++-- .../components/withings/.translations/fr.json | 1 + .../components/withings/.translations/sv.json | 23 ++++- .../components/wled/.translations/sv.json | 26 +++++ .../components/zha/.translations/sv.json | 35 +++++++ 87 files changed, 1537 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/abode/.translations/sv.json create mode 100644 homeassistant/components/airly/.translations/sv.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/sv.json create mode 100644 homeassistant/components/arcam_fmj/.translations/sv.json create mode 100644 homeassistant/components/binary_sensor/.translations/sv.json create mode 100644 homeassistant/components/cert_expiry/.translations/sv.json create mode 100644 homeassistant/components/climate/.translations/sv.json create mode 100644 homeassistant/components/coolmaster/.translations/sv.json create mode 100644 homeassistant/components/cover/.translations/sv.json create mode 100644 homeassistant/components/demo/.translations/sv.json create mode 100644 homeassistant/components/device_tracker/.translations/sv.json create mode 100644 homeassistant/components/elgato/.translations/sv.json create mode 100644 homeassistant/components/fan/.translations/sv.json create mode 100644 homeassistant/components/garmin_connect/.translations/fr.json create mode 100644 homeassistant/components/gdacs/.translations/ca.json create mode 100644 homeassistant/components/gdacs/.translations/da.json create mode 100644 homeassistant/components/gdacs/.translations/de.json create mode 100644 homeassistant/components/gdacs/.translations/it.json create mode 100644 homeassistant/components/gdacs/.translations/ko.json create mode 100644 homeassistant/components/gdacs/.translations/ru.json create mode 100644 homeassistant/components/gdacs/.translations/sv.json create mode 100644 homeassistant/components/gdacs/.translations/zh-Hant.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/sv.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/sv.json create mode 100644 homeassistant/components/gios/.translations/sv.json create mode 100644 homeassistant/components/glances/.translations/sv.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/sv.json create mode 100644 homeassistant/components/iaqualink/.translations/sv.json create mode 100644 homeassistant/components/izone/.translations/sv.json create mode 100644 homeassistant/components/light/.translations/sv.json create mode 100644 homeassistant/components/local_ip/.translations/sv.json create mode 100644 homeassistant/components/lock/.translations/sv.json create mode 100644 homeassistant/components/lutron_caseta/.translations/sv.json create mode 100644 homeassistant/components/meteo_france/.translations/fr.json create mode 100644 homeassistant/components/meteo_france/.translations/it.json create mode 100644 homeassistant/components/mikrotik/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/sv.json create mode 100644 homeassistant/components/opentherm_gw/.translations/sv.json create mode 100644 homeassistant/components/sensor/.translations/sv.json create mode 100644 homeassistant/components/sentry/.translations/sv.json create mode 100644 homeassistant/components/solaredge/.translations/sv.json create mode 100644 homeassistant/components/solarlog/.translations/sv.json create mode 100644 homeassistant/components/soma/.translations/sv.json create mode 100644 homeassistant/components/switch/.translations/sv.json create mode 100644 homeassistant/components/tesla/.translations/sv.json create mode 100644 homeassistant/components/traccar/.translations/sv.json create mode 100644 homeassistant/components/twentemilieu/.translations/sv.json create mode 100644 homeassistant/components/vacuum/.translations/sv.json create mode 100644 homeassistant/components/velbus/.translations/sv.json create mode 100644 homeassistant/components/vesync/.translations/sv.json create mode 100644 homeassistant/components/wled/.translations/sv.json diff --git a/homeassistant/components/abode/.translations/sv.json b/homeassistant/components/abode/.translations/sv.json new file mode 100644 index 00000000000..9a59e4c2007 --- /dev/null +++ b/homeassistant/components/abode/.translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." + }, + "error": { + "connection_error": "Det gick inte att ansluta till Abode.", + "identifier_exists": "Kontot \u00e4r redan registrerat.", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter." + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Fyll i din inloggningsinformation f\u00f6r Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/sv.json b/homeassistant/components/adguard/.translations/sv.json index 22bd81e3e97..519ecef52db 100644 --- a/homeassistant/components/adguard/.translations/sv.json +++ b/homeassistant/components/adguard/.translations/sv.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}. Uppdatera ditt Hass.io AdGuard Home-till\u00e4gg.", + "adguard_home_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}.", "existing_instance_updated": "Uppdaterade existerande konfiguration.", "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten." }, diff --git a/homeassistant/components/airly/.translations/sv.json b/homeassistant/components/airly/.translations/sv.json new file mode 100644 index 00000000000..5b81b4625a2 --- /dev/null +++ b/homeassistant/components/airly/.translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." + }, + "error": { + "auth": "API-nyckeln \u00e4r inte korrekt.", + "name_exists": "Namnet finns redan.", + "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Integrationens namn" + }, + "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sv.json b/homeassistant/components/alarm_control_panel/.translations/sv.json new file mode 100644 index 00000000000..65e4433f5a3 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Larma {entity_name} borta", + "arm_home": "Larma {entity_name} hemma", + "arm_night": "Larma {entity_name} natt", + "disarm": "Avlarma {entity_name}", + "trigger": "Utl\u00f6sare {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} larmad borta", + "armed_home": "{entity_name} larmad hemma", + "armed_night": "{entity_name} larmad natt", + "disarmed": "{entity_name} bortkopplad", + "triggered": "{entity_name} utl\u00f6st" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/sv.json b/homeassistant/components/almond/.translations/sv.json index 61af3a04e47..d2630b95c02 100644 --- a/homeassistant/components/almond/.translations/sv.json +++ b/homeassistant/components/almond/.translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_setup": "Du kan bara konfigurera ett Almond-konto.", + "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", + "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond." + }, "step": { "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Hass.io-till\u00e4gget: {addon} ?", "title": "Almond via Hass.io-till\u00e4gget" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" } - } + }, + "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/sv.json b/homeassistant/components/arcam_fmj/.translations/sv.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/sv.json b/homeassistant/components/axis/.translations/sv.json index a38ef2ef745..95c7e23f50e 100644 --- a/homeassistant/components/axis/.translations/sv.json +++ b/homeassistant/components/axis/.translations/sv.json @@ -4,7 +4,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "bad_config_file": "Felaktig data fr\u00e5n config fil", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet" + "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet", + "updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress" }, "error": { "already_configured": "Enheten \u00e4r redan konfigurerad", @@ -12,6 +13,7 @@ "device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig", "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter" }, + "flow_title": "Axisenhet: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/sv.json b/homeassistant/components/binary_sensor/.translations/sv.json new file mode 100644 index 00000000000..5df2ce17c92 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/sv.json @@ -0,0 +1,94 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt", + "is_cold": "{entity_name} \u00e4r kall", + "is_connected": "{entity_name} \u00e4r ansluten", + "is_gas": "{entity_name} detekterar gas", + "is_hot": "{entity_name} \u00e4r varm", + "is_light": "{entity_name} uppt\u00e4cker ljus", + "is_locked": "{entity_name} \u00e4r l\u00e5st", + "is_moist": "{entity_name} \u00e4r fuktig", + "is_motion": "{entity_name} detekterar r\u00f6relse", + "is_moving": "{entity_name} r\u00f6r sig", + "is_no_gas": "{entity_name} uppt\u00e4cker inte gas", + "is_no_light": "{entity_name} uppt\u00e4cker inte ljus", + "is_no_motion": "{entity_name} detekterar inte r\u00f6relse", + "is_no_problem": "{entity_name} uppt\u00e4cker inte problem", + "is_no_smoke": "{entity_name} detekterar inte r\u00f6k", + "is_no_sound": "{entity_name} uppt\u00e4cker inte ljud", + "is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer", + "is_not_bat_low": "{entity_name} batteri \u00e4r normalt", + "is_not_cold": "{entity_name} \u00e4r inte kall", + "is_not_connected": "{entity_name} \u00e4r fr\u00e5nkopplad", + "is_not_hot": "{entity_name} \u00e4r inte varm", + "is_not_locked": "{entity_name} \u00e4r ol\u00e5st", + "is_not_moist": "{entity_name} \u00e4r torr", + "is_not_moving": "{entity_name} r\u00f6r sig inte", + "is_not_occupied": "{entity_name} \u00e4r inte upptagen", + "is_not_open": "{entity_name} \u00e4r st\u00e4ngd", + "is_not_plugged_in": "{entity_name} \u00e4r urkopplad", + "is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd", + "is_not_present": "{entity_name} finns inte", + "is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker", + "is_occupied": "{entity_name} \u00e4r upptagen", + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5", + "is_open": "{entity_name} \u00e4r \u00f6ppen", + "is_plugged_in": "{entity_name} \u00e4r ansluten", + "is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd", + "is_present": "{entity_name} \u00e4r n\u00e4rvarande", + "is_problem": "{entity_name} uppt\u00e4cker problem", + "is_smoke": "{entity_name} detekterar r\u00f6k", + "is_sound": "{entity_name} uppt\u00e4cker ljud", + "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", + "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" + }, + "trigger_type": { + "bat_low": "{entity_name} batteri l\u00e5gt", + "closed": "{entity_name} st\u00e4ngd", + "cold": "{entity_name} blev kall", + "connected": "{entity_name} ansluten", + "gas": "{entity_name} b\u00f6rjade detektera gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus", + "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} blev fuktig", + "moist\u00a7": "{entity_name} blev fuktig", + "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse", + "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig", + "no_gas": "{entity_name} slutade uppt\u00e4cka gas", + "no_light": "{entity_name} slutade uppt\u00e4cka ljus", + "no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse", + "no_problem": "{entity_name} slutade uppt\u00e4cka problem", + "no_smoke": "{entity_name} slutade detektera r\u00f6k", + "no_sound": "{entity_name} slutade uppt\u00e4cka ljud", + "no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev inte kall", + "not_connected": "{entity_name} fr\u00e5nkopplad", + "not_hot": "{entity_name} blev inte varm", + "not_locked": "{entity_name} ol\u00e5st", + "not_moist": "{entity_name} blev torr", + "not_moving": "{entity_name} slutade r\u00f6ra sig", + "not_occupied": "{entity_name} blev inte upptagen", + "not_opened": "{entity_name} st\u00e4ngd", + "not_plugged_in": "{entity_name} urkopplad", + "not_powered": "{entity_name} inte str\u00f6mf\u00f6rd", + "not_present": "{entity_name} inte n\u00e4rvarande", + "not_unsafe": "{entity_name} blev s\u00e4ker", + "occupied": "{entity_name} blev upptagen", + "opened": "{entity_name} \u00f6ppnades", + "plugged_in": "{entity_name} ansluten", + "powered": "{entity_name} str\u00f6mf\u00f6rd", + "present": "{entity_name} n\u00e4rvarande", + "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem", + "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k", + "sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5", + "unsafe": "{entity_name} blev os\u00e4ker", + "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/fr.json b/homeassistant/components/brother/.translations/fr.json index 99d49cc3bd8..788d0c74003 100644 --- a/homeassistant/components/brother/.translations/fr.json +++ b/homeassistant/components/brother/.translations/fr.json @@ -9,6 +9,7 @@ "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." }, + "flow_title": "Imprimante Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -21,7 +22,9 @@ "zeroconf_confirm": { "data": { "type": "Type d'imprimante" - } + }, + "description": "Voulez-vous ajouter l'imprimante Brother {model} avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant ?", + "title": "Imprimante Brother d\u00e9couverte" } }, "title": "Imprimante Brother" diff --git a/homeassistant/components/brother/.translations/sv.json b/homeassistant/components/brother/.translations/sv.json index 8661c3278bc..774863d4f08 100644 --- a/homeassistant/components/brother/.translations/sv.json +++ b/homeassistant/components/brother/.translations/sv.json @@ -1,11 +1,32 @@ { "config": { + "abort": { + "already_configured": "Den h\u00e4r skrivaren \u00e4r redan konfigurerad.", + "unsupported_model": "Den h\u00e4r skrivarmodellen st\u00f6ds inte." + }, + "error": { + "connection_error": "Anslutningsfel.", + "snmp_error": "SNMP-servern har st\u00e4ngts av eller s\u00e5 st\u00f6ds inte skrivaren.", + "wrong_host": "Ogiltigt v\u00e4rdnamn eller IP-adress." + }, + "flow_title": "Brother-skrivare: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Skrivarens v\u00e4rdnamn eller IP-adress", + "type": "Typ av skrivare" + }, + "description": "St\u00e4ll in Brother-skrivarintegration. Om du har problem med konfigurationen g\u00e5r du till: https://www.home-assistant.io/integrations/brother", + "title": "Brother-skrivare" + }, "zeroconf_confirm": { "data": { "type": "Typ av skrivare" - } + }, + "description": "Vill du l\u00e4gga till Brother-skrivaren {model} med serienumret {serial_number} i Home Assistant?", + "title": "Uppt\u00e4ckte Brother-skrivare" } - } + }, + "title": "Brother-skrivare" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/sv.json b/homeassistant/components/cert_expiry/.translations/sv.json new file mode 100644 index 00000000000..bdccf51b2cd --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad" + }, + "error": { + "certificate_error": "Certifikatet kunde inte valideras", + "certificate_fetch_failed": "Kan inte h\u00e4mta certifikat fr\u00e5n denna v\u00e4rd- och portkombination", + "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden", + "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad", + "resolve_failed": "Denna v\u00e4rd kan inte resolveras", + "wrong_host": "Certifikatet matchar inte v\u00e4rdnamnet" + }, + "step": { + "user": { + "data": { + "host": "Certifikatets v\u00e4rdnamn", + "name": "Certifikatets namn", + "port": "Certifikatets port" + }, + "title": "Definiera certifikatet som ska testas" + } + }, + "title": "Certifikatets utg\u00e5ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/sv.json b/homeassistant/components/climate/.translations/sv.json new file mode 100644 index 00000000000..51fe0540549 --- /dev/null +++ b/homeassistant/components/climate/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u00c4ndra HVAC-l\u00e4ge p\u00e5 {entity_name}", + "set_preset_mode": "\u00c4ndra f\u00f6rinst\u00e4llning p\u00e5 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt HVAC-l\u00e4ge", + "is_preset_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt f\u00f6rinst\u00e4llt l\u00e4ge" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} uppm\u00e4tt fuktighet har \u00e4ndrats", + "current_temperature_changed": "{entity_name} uppm\u00e4tt temperatur har \u00e4ndrats", + "hvac_mode_changed": "{entity_name} HVAC-l\u00e4ge har \u00e4ndrats" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/sv.json b/homeassistant/components/coolmaster/.translations/sv.json new file mode 100644 index 00000000000..89e2ab32863 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Det gick inte att ansluta till CoolMasterNet-instansen. Kontrollera din v\u00e4rd.", + "no_units": "Det gick inte att hitta n\u00e5gra HVAC-enheter i CoolMasterNet-v\u00e4rden." + }, + "step": { + "user": { + "data": { + "cool": "St\u00f6d svalt l\u00e4ge", + "dry": "St\u00f6d torrl\u00e4ge", + "fan_only": "St\u00f6d endast fl\u00e4ktl\u00e4ge", + "heat": "St\u00f6d v\u00e4rmel\u00e4ge", + "heat_cool": "St\u00f6d automatiskt v\u00e4rme/kyl-l\u00e4ge", + "host": "V\u00e4rd", + "off": "Kan st\u00e4ngas av" + }, + "title": "St\u00e4ll in dina CoolMasterNet-anslutningsdetaljer." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/sv.json b/homeassistant/components/cover/.translations/sv.json new file mode 100644 index 00000000000..906768d3eb3 --- /dev/null +++ b/homeassistant/components/cover/.translations/sv.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u00e4r st\u00e4ngd", + "is_closing": "{entity_name} st\u00e4ngs", + "is_open": "{entity_name} \u00e4r \u00f6ppen", + "is_opening": "{entity_name} \u00f6ppnas", + "is_position": "Aktuell position f\u00f6r {entity_name} \u00e4r", + "is_tilt_position": "Aktuell {entity_name} lutningsposition \u00e4r" + }, + "trigger_type": { + "closed": "{entity_name} st\u00e4ngd", + "closing": "{entity_name} st\u00e4nger", + "opened": "{entity_name} \u00f6ppnades", + "opening": "{entity_name} \u00f6ppnas", + "position": "{entity_name} position \u00e4ndras", + "tilt_position": "{entity_name} lutningsposition \u00e4ndras" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index 02869dcf76e..3d74d6cb944 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -11,6 +11,7 @@ "error": { "no_key": "Det gick inte att ta emot en API-nyckel" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { @@ -40,5 +41,75 @@ } }, "title": "deCONZ Zigbee Gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e5da knapparna", + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "close": "St\u00e4ng", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "left": "V\u00e4nster", + "open": "\u00d6ppen", + "right": "H\u00f6ger", + "side_1": "Sida 1", + "side_2": "Sida 2", + "side_3": "Sida 3", + "side_4": "Sida 4", + "side_5": "Sida 5", + "side_6": "Sida 6", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + }, + "trigger_type": { + "remote_awakened": "Enheten v\u00e4cktes", + "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", + "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", + "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", + "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", + "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", + "remote_button_rotated": "Knappen roterade \"{subtype}\"", + "remote_button_rotation_stopped": "Knapprotationen \"{subtype}\" stoppades", + "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", + "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickad", + "remote_double_tap": "Enheten \"{subtype}\" dubbeltryckt", + "remote_double_tap_any_side": "Enheten dubbeltryckt p\u00e5 valfri sida", + "remote_falling": "Enhet i fritt fall", + "remote_flip_180_degrees": "Enheten v\u00e4nd 180 grader", + "remote_flip_90_degrees": "Enheten v\u00e4nd 90 grader", + "remote_gyro_activated": "Enhet skakad", + "remote_moved": "Enheten flyttades med \"{subtype}\" upp", + "remote_moved_any_side": "Enheten flyttades med valfri sida upp\u00e5t", + "remote_rotate_from_side_1": "Enheten roterades fr\u00e5n \"sida 1\" till \"{subtype}\"", + "remote_rotate_from_side_2": "Enheten roterades fr\u00e5n \"sida 2\" till \"{subtype}\"", + "remote_rotate_from_side_3": "Enheten roterades fr\u00e5n \"sida 3\" till \"{subtype}\"", + "remote_rotate_from_side_4": "Enheten roterades fr\u00e5n \"sida 4\" till \"{subtype}\"", + "remote_rotate_from_side_5": "Enheten roterades fr\u00e5n \"sida 5\" till \"{subtype}\"", + "remote_rotate_from_side_6": "Enheten roterades fr\u00e5n \"sida 6\" till \"{subtype}\"", + "remote_turned_clockwise": "Enheten vriden medurs", + "remote_turned_counter_clockwise": "Enheten v\u00e4nde moturs" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", + "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" + }, + "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", + "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" + }, + "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/sv.json b/homeassistant/components/demo/.translations/sv.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/sv.json b/homeassistant/components/device_tracker/.translations/sv.json new file mode 100644 index 00000000000..70287ad318a --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/sv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u00e4r hemma", + "is_not_home": "{entity_name} \u00e4r inte hemma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sv.json b/homeassistant/components/ecobee/.translations/sv.json index f4a63bb449d..da62172dc10 100644 --- a/homeassistant/components/ecobee/.translations/sv.json +++ b/homeassistant/components/ecobee/.translations/sv.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "one_instance_only": "Denna integration st\u00f6der f\u00f6r n\u00e4rvarande endast en ecobee-instans." + }, + "error": { + "pin_request_failed": "Fel vid beg\u00e4ran av PIN-kod fr\u00e5n ecobee. kontrollera API-nyckeln \u00e4r korrekt.", + "token_request_failed": "Fel vid beg\u00e4ran av tokens fr\u00e5n ecobee; v\u00e4nligen f\u00f6rs\u00f6k igen." + }, "step": { + "authorize": { + "description": "V\u00e4nligen auktorisera denna app p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kod:\n\n{pin}\n\nTryck sedan p\u00e5 Skicka.", + "title": "Auktorisera app p\u00e5 ecobee.com" + }, "user": { "data": { "api_key": "API-nyckel" - } + }, + "description": "V\u00e4nligen ange API-nyckeln som erh\u00e5llits fr\u00e5n ecobee.com.", + "title": "ecobee API-nyckel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/sv.json b/homeassistant/components/elgato/.translations/sv.json new file mode 100644 index 00000000000..83850c186c7 --- /dev/null +++ b/homeassistant/components/elgato/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad.", + "connection_error": "Det gick inte att ansluta till Elgato Key Light-enheten." + }, + "error": { + "connection_error": "Det gick inte att ansluta till Elgato Key Light-enheten." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress", + "port": "Portnummer" + }, + "description": "St\u00e4ll in ditt Elgato Key Light f\u00f6r att integrera med Home Assistant.", + "title": "L\u00e4nk din Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Elgato Key Light med serienummer `{serial_number}` till Home Assistant?", + "title": "Uppt\u00e4ckte Elgato Key Light-enhet" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/sv.json b/homeassistant/components/fan/.translations/sv.json new file mode 100644 index 00000000000..c080d1b364b --- /dev/null +++ b/homeassistant/components/fan/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} aktiverades" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/fr.json b/homeassistant/components/garmin_connect/.translations/fr.json new file mode 100644 index 00000000000..f0dd8a79e5b --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer.", + "invalid_auth": "Authentification non valide.", + "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", + "unknown": "Erreur inattendue." + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Entrez vos informations d'identification.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/ca.json b/homeassistant/components/gdacs/.translations/ca.json new file mode 100644 index 00000000000..5f5acfe7ccf --- /dev/null +++ b/homeassistant/components/gdacs/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada." + }, + "step": { + "user": { + "data": { + "radius": "Radi" + }, + "title": "Introducci\u00f3 dels detalls del filtre." + } + }, + "title": "Sistema Global de Coordinaci\u00f3 i Alerta per Desastres (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/da.json b/homeassistant/components/gdacs/.translations/da.json new file mode 100644 index 00000000000..64f3dd000c4 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Lokaliteten er allerede konfigureret." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "Globalt katastrofevarslings- og koordineringssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/de.json b/homeassistant/components/gdacs/.translations/de.json new file mode 100644 index 00000000000..12f94250402 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Der Standort ist bereits konfiguriert." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00fclle deine Filterangaben aus." + } + }, + "title": "Globales Katastrophenalarm- und Koordinierungssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/it.json b/homeassistant/components/gdacs/.translations/it.json new file mode 100644 index 00000000000..249b47f9f59 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata." + }, + "step": { + "user": { + "data": { + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "Sistema globale di allerta e coordinamento delle catastrofi (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/ko.json b/homeassistant/components/gdacs/.translations/ko.json new file mode 100644 index 00000000000..10d6f73e56f --- /dev/null +++ b/homeassistant/components/gdacs/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." + } + }, + "title": "\uad6d\uc81c \uc7ac\ub09c \uacbd\ubcf4 \ubc0f \uc870\uc815 \uae30\uad6c (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/ru.json b/homeassistant/components/gdacs/.translations/ru.json new file mode 100644 index 00000000000..f006832b5be --- /dev/null +++ b/homeassistant/components/gdacs/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + }, + "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0446\u0438\u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0442\u0438\u0445\u0438\u0439\u043d\u044b\u0445 \u0431\u0435\u0434\u0441\u0442\u0432\u0438\u0439 (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/sv.json b/homeassistant/components/gdacs/.translations/sv.json new file mode 100644 index 00000000000..3c7fb00056e --- /dev/null +++ b/homeassistant/components/gdacs/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad." + }, + "step": { + "user": { + "data": { + "radius": "Radie" + }, + "title": "Fyll i filterinformation." + } + }, + "title": "Globalt katastrofvarnings- och samordningssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/zh-Hant.json b/homeassistant/components/gdacs/.translations/zh-Hant.json new file mode 100644 index 00000000000..59f9b7be031 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u4f4d\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "user": { + "data": { + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "\u5168\u7403\u707d\u96e3\u9810\u8b66\u548c\u5354\u8abf\u7cfb\u7d71\uff08GDACS\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json index 26caa2ebe54..66a216149dd 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ko.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "\ubc18\uacbd" }, - "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." } }, "title": "GeoNet NZ Quakes" diff --git a/homeassistant/components/geonetnz_quakes/.translations/sv.json b/homeassistant/components/geonetnz_quakes/.translations/sv.json new file mode 100644 index 00000000000..13058ad3ad2 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Plats redan registrerad" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radie" + }, + "title": "Fyll i dina filterdetaljer." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ko.json b/homeassistant/components/geonetnz_volcano/.translations/ko.json index 5d393fef4c4..d19091e75e8 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/ko.json +++ b/homeassistant/components/geonetnz_volcano/.translations/ko.json @@ -8,7 +8,7 @@ "data": { "radius": "\ubc18\uacbd" }, - "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." } }, "title": "GeoNet NZ Volcano" diff --git a/homeassistant/components/geonetnz_volcano/.translations/sv.json b/homeassistant/components/geonetnz_volcano/.translations/sv.json new file mode 100644 index 00000000000..35e7e24c926 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Plats redan registrerad" + }, + "step": { + "user": { + "data": { + "radius": "Radie" + }, + "title": "Fyll i dina filterdetaljer." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/sv.json b/homeassistant/components/gios/.translations/sv.json new file mode 100644 index 00000000000..b5a865b5ccd --- /dev/null +++ b/homeassistant/components/gios/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a-integration f\u00f6r denna m\u00e4tstation \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till GIO\u015a-servern.", + "invalid_sensors_data": "Ogiltig sensordata f\u00f6r denna m\u00e4tstation.", + "wrong_station_id": "M\u00e4tstationens ID \u00e4r inte korrekt." + }, + "step": { + "user": { + "data": { + "name": "Integrationens namn", + "station_id": "M\u00e4tstationens ID" + }, + "description": "St\u00e4ll in luftkvalitetintegration f\u00f6r GIO\u015a (polsk chefinspektorat f\u00f6r milj\u00f6skydd). Om du beh\u00f6ver hj\u00e4lp med konfigurationen titta h\u00e4r: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/sv.json b/homeassistant/components/glances/.translations/sv.json new file mode 100644 index 00000000000..f4b95081a10 --- /dev/null +++ b/homeassistant/components/glances/.translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "wrong_version": "Version st\u00f6ds inte (endast 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "ssl": "Anv\u00e4nd SSL / TLS f\u00f6r att ansluta till Glances-systemet", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera certifieringen av systemet", + "version": "Glances API-version (2 eller 3)" + }, + "title": "St\u00e4ll in Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + }, + "description": "Konfigurera alternativ f\u00f6r Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/sv.json b/homeassistant/components/hisense_aehw4a1/.translations/sv.json new file mode 100644 index 00000000000..6ec35452e8b --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Hisense AEH-W4A1-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Hisense AEH-W4A1 \u00e4r m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/sv.json b/homeassistant/components/huawei_lte/.translations/sv.json index fb73612d897..16b192d16a1 100644 --- a/homeassistant/components/huawei_lte/.translations/sv.json +++ b/homeassistant/components/huawei_lte/.translations/sv.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r enheten har redan konfigurerats" + "already_configured": "Den h\u00e4r enheten har redan konfigurerats", + "already_in_progress": "Den h\u00e4r enheten har redan konfigurerats", + "not_huawei_lte": "Inte en Huawei LTE-enhet" + }, + "error": { + "connection_failed": "Anslutningen misslyckades", + "connection_timeout": "Timeout f\u00f6r anslutning", + "incorrect_password": "Felaktigt l\u00f6senord", + "incorrect_username": "Felaktigt anv\u00e4ndarnamn", + "incorrect_username_or_password": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord", + "invalid_url": "Ogiltig URL", + "login_attempts_exceeded": "Maximala inloggningsf\u00f6rs\u00f6k har \u00f6verskridits, f\u00f6rs\u00f6k igen senare", + "response_error": "Ok\u00e4nt fel fr\u00e5n enheten", + "unknown_connection_error": "Ok\u00e4nt fel vid anslutning till enheten" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "url": "URL", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange information om enhets\u00e5tkomst. Det \u00e4r valfritt att ange anv\u00e4ndarnamn och l\u00f6senord, men st\u00f6djer d\u00e5 fler integrationsfunktioner. \u00c5 andra sidan kan anv\u00e4ndning av en auktoriserad anslutning orsaka problem med att komma \u00e5t enhetens webbgr\u00e4nssnitt utanf\u00f6r Home Assistant medan integrationen \u00e4r aktiv och tv\u00e4rtom.", + "title": "Konfigurera Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "name": "Namn p\u00e5 meddelandetj\u00e4nsten (\u00e4ndring kr\u00e4ver omstart)", + "recipient": "Mottagare av SMS-meddelanden", + "track_new_devices": "Sp\u00e5ra nya enheter" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/sv.json b/homeassistant/components/iaqualink/.translations/sv.json new file mode 100644 index 00000000000..aa2b4142616 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bara konfigurera en enda iAqualink-anslutning." + }, + "error": { + "connection_failure": "Det g\u00e5r inte att ansluta till iAqualink. Kontrollera ditt anv\u00e4ndarnamn och l\u00f6senord." + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn / E-postadress" + }, + "description": "V\u00e4nligen ange anv\u00e4ndarnamn och l\u00f6senord f\u00f6r ditt iAqualink-konto.", + "title": "Anslut till iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/sv.json b/homeassistant/components/icloud/.translations/sv.json index 8c4c45f9c89..fc5b81b6591 100644 --- a/homeassistant/components/icloud/.translations/sv.json +++ b/homeassistant/components/icloud/.translations/sv.json @@ -1,5 +1,37 @@ { "config": { - "title": "" + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "login": "Inloggningsfel: var god att kontrollera din e-postadress och l\u00f6senord", + "send_verification_code": "Det gick inte att skicka verifieringskod", + "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Betrodd enhet" + }, + "description": "V\u00e4lj din betrodda enhet", + "title": "Betrodd iCloud-enhet" + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-post" + }, + "description": "Ange dina autentiseringsuppgifter", + "title": "iCloud-autentiseringsuppgifter" + }, + "verification_code": { + "data": { + "verification_code": "Verifieringskod" + }, + "description": "V\u00e4nligen ange verifieringskoden som du just f\u00e5tt fr\u00e5n iCloud", + "title": "iCloud-verifieringskod" + } + }, + "title": "Apple iCloud" } } \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/sv.json b/homeassistant/components/izone/.translations/sv.json new file mode 100644 index 00000000000..c2c952d69fe --- /dev/null +++ b/homeassistant/components/izone/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga iZone-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av iZone \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/sv.json b/homeassistant/components/life360/.translations/sv.json index 836680aad6a..ba28d973ec3 100644 --- a/homeassistant/components/life360/.translations/sv.json +++ b/homeassistant/components/life360/.translations/sv.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ogiltiga autentiseringsuppgifter", "invalid_username": "Ogiltigt anv\u00e4ndarnmn", + "unexpected": "Ov\u00e4ntat fel vid kommunikation med Life360-servern", "user_already_configured": "Konto har redan konfigurerats" }, "step": { diff --git a/homeassistant/components/light/.translations/sv.json b/homeassistant/components/light/.translations/sv.json new file mode 100644 index 00000000000..8df3f3d382b --- /dev/null +++ b/homeassistant/components/light/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "V\u00e4xla {entity_name}", + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} avst\u00e4ngd", + "turned_on": "{entity_name} slogs p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/sv.json b/homeassistant/components/linky/.translations/sv.json index 4e7be709482..4880e065fa2 100644 --- a/homeassistant/components/linky/.translations/sv.json +++ b/homeassistant/components/linky/.translations/sv.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Kontot har redan konfigurerats." - } + }, + "error": { + "access": "Det gick inte att komma \u00e5t Enedis.fr, kontrollera din internetanslutning", + "enedis": "Enedis.fr svarade med ett fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)", + "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)", + "wrong_login": "Inloggningsfel: v\u00e4nligen kontrollera din e-post och l\u00f6senord" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-post" + }, + "description": "Ange dina autentiseringsuppgifter", + "title": "Linky" + } + }, + "title": "Linky" } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/sv.json b/homeassistant/components/local_ip/.translations/sv.json new file mode 100644 index 00000000000..d9f9b474f9c --- /dev/null +++ b/homeassistant/components/local_ip/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integrationen \u00e4r redan konfigurerad med en befintlig sensor med det namnet" + }, + "step": { + "user": { + "data": { + "name": "Sensor Namn" + }, + "title": "Lokal IP-adress" + } + }, + "title": "Lokal IP-adress" + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/sv.json b/homeassistant/components/lock/.translations/sv.json new file mode 100644 index 00000000000..7d50b4ea61a --- /dev/null +++ b/homeassistant/components/lock/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00d6ppna {entity_name}", + "unlock": "L\u00e5s upp {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} \u00e4r l\u00e5st", + "is_unlocked": "{entity_name} \u00e4r ol\u00e5st" + }, + "trigger_type": { + "locked": "{entity_name} l\u00e5st", + "unlocked": "{entity_name} ol\u00e5st" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/sv.json b/homeassistant/components/lutron_caseta/.translations/sv.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/sv.json b/homeassistant/components/met/.translations/sv.json index aa860e27307..d8b461913da 100644 --- a/homeassistant/components/met/.translations/sv.json +++ b/homeassistant/components/met/.translations/sv.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Namnet finns redan" + "name_exists": "Plats finns redan" }, "step": { "user": { diff --git a/homeassistant/components/meteo_france/.translations/ca.json b/homeassistant/components/meteo_france/.translations/ca.json index 6f2fd707045..aeceb80a063 100644 --- a/homeassistant/components/meteo_france/.translations/ca.json +++ b/homeassistant/components/meteo_france/.translations/ca.json @@ -1,10 +1,15 @@ { "config": { + "abort": { + "already_configured": "Ciutat ja configurada", + "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard" + }, "step": { "user": { "data": { "city": "Ciutat" }, + "description": "Introdueix el codi postal (nom\u00e9s recomanat per Fran\u00e7a) o nom de la ciutat", "title": "M\u00e9t\u00e9o-France" } }, diff --git a/homeassistant/components/meteo_france/.translations/de.json b/homeassistant/components/meteo_france/.translations/de.json index 0e99c1de0ce..8f05ad18df3 100644 --- a/homeassistant/components/meteo_france/.translations/de.json +++ b/homeassistant/components/meteo_france/.translations/de.json @@ -9,6 +9,7 @@ "data": { "city": "Stadt" }, + "description": "Geben Sie die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein", "title": "M\u00e9t\u00e9o-France" } }, diff --git a/homeassistant/components/meteo_france/.translations/fr.json b/homeassistant/components/meteo_france/.translations/fr.json new file mode 100644 index 00000000000..7dff0d237fd --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ville d\u00e9j\u00e0 configur\u00e9e", + "unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard" + }, + "step": { + "user": { + "data": { + "city": "Ville" + }, + "description": "Entrez le code postal (uniquement pour la France, recommand\u00e9) ou le nom de la ville", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/it.json b/homeassistant/components/meteo_france/.translations/it.json new file mode 100644 index 00000000000..5a067430906 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Citt\u00e0 gi\u00e0 configurata", + "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi" + }, + "step": { + "user": { + "data": { + "city": "Citt\u00e0" + }, + "description": "Inserisci il codice postale (solo per la Francia, consigliato) o il nome della citt\u00e0", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/fr.json b/homeassistant/components/mikrotik/.translations/fr.json new file mode 100644 index 00000000000..220da6fcbaf --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/fr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de la connexion", + "name_exists": "Le nom existe", + "wrong_credentials": "Identifiants erron\u00e9s" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur", + "verify_ssl": "Utiliser SSL" + }, + "title": "Configurer le routeur Mikrotik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Activer le ping ARP", + "force_dhcp": "Forcer l'analyse \u00e0 l'aide de DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/sv.json b/homeassistant/components/mikrotik/.translations/sv.json index 572b159221d..7be080d96a2 100644 --- a/homeassistant/components/mikrotik/.translations/sv.json +++ b/homeassistant/components/mikrotik/.translations/sv.json @@ -1,10 +1,37 @@ { "config": { + "abort": { + "already_configured": "Mikrotik \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Anslutningen misslyckades", + "name_exists": "Namnet finns", + "wrong_credentials": "Fel autentiseringsuppgifter" + }, "step": { "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Anv\u00e4nd ssl" + }, "title": "Konfigurera Mikrotik-router" } }, "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Aktivera ARP-ping", + "detection_time": "Intervall f\u00f6r att betraktas som hemma", + "force_dhcp": "Tvinga skanning med DHCP" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/sv.json b/homeassistant/components/neato/.translations/sv.json new file mode 100644 index 00000000000..64edf9e93ce --- /dev/null +++ b/homeassistant/components/neato/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Redan konfigurerad", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" + }, + "create_entry": { + "default": "Se [Neato-dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ogiltiga autentiseringsuppgifter", + "unexpected_error": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn", + "vendor": "Leverant\u00f6r" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato-kontoinfo" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sv.json b/homeassistant/components/opentherm_gw/.translations/sv.json new file mode 100644 index 00000000000..89ce4d75674 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sv.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "already_configured": "Gateway redan konfigurerad", + "id_exists": "Gateway-id finns redan", + "serial_error": "Fel vid anslutning till enheten", + "timeout": "Anslutningsf\u00f6rs\u00f6ket avbr\u00f6ts" + }, + "step": { + "init": { + "data": { + "device": "S\u00f6kv\u00e4g eller URL", + "floor_temperature": "Golvtemperatur", + "id": "ID", + "name": "Namn", + "precision": "Klimatemperaturprecision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Golvetemperatur", + "precision": "Precision" + }, + "description": "Alternativ f\u00f6r OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json index 702cec128c0..25152e9dc81 100644 --- a/homeassistant/components/plex/.translations/sv.json +++ b/homeassistant/components/plex/.translations/sv.json @@ -1,10 +1,62 @@ { + "config": { + "abort": { + "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats", + "already_configured": "Denna Plex-server \u00e4r redan konfigurerad", + "already_in_progress": "Plex konfigureras", + "discovery_no_file": "Ingen \u00e4ldre konfigurationsfil hittades", + "invalid_import": "Importerad konfiguration \u00e4r ogiltig", + "non-interactive": "Icke-interaktiv import", + "token_request_timeout": "Timeout att erh\u00e5lla token", + "unknown": "Misslyckades av ok\u00e4nd anledning" + }, + "error": { + "faulty_credentials": "Auktoriseringen misslyckades", + "no_servers": "Inga servrar l\u00e4nkade till konto", + "no_token": "Ange en token eller v\u00e4lj manuell inst\u00e4llning", + "not_found": "Plex-server hittades inte" + }, + "step": { + "manual_setup": { + "data": { + "host": "V\u00e4rd", + "port": "Port", + "ssl": "Anv\u00e4nd SSL", + "token": "Token (om det beh\u00f6vs)", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "title": "Plex-server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "V\u00e4lj flera servrar tillg\u00e4ngliga, v\u00e4lj en:", + "title": "V\u00e4lj Plex-server" + }, + "start_website_auth": { + "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv.", + "title": "Anslut Plex-servern" + }, + "user": { + "data": { + "manual_setup": "Manuell inst\u00e4llning", + "token": "Plex-token" + }, + "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv eller konfigurera en server manuellt.", + "title": "Anslut Plex-servern" + } + }, + "title": "Plex" + }, "options": { "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Visa alla kontroller" - } + "show_all_controls": "Visa alla kontroller", + "use_episode_art": "Anv\u00e4nd avsnittsbild" + }, + "description": "Alternativ f\u00f6r Plex-mediaspelare" } } } diff --git a/homeassistant/components/ring/.translations/sv.json b/homeassistant/components/ring/.translations/sv.json index 54e9f5200f2..e92790740fb 100644 --- a/homeassistant/components/ring/.translations/sv.json +++ b/homeassistant/components/ring/.translations/sv.json @@ -4,6 +4,7 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json index d938373797e..7ca5879a5c0 100644 --- a/homeassistant/components/samsungtv/.translations/ca.json +++ b/homeassistant/components/samsungtv/.translations/ca.json @@ -5,8 +5,10 @@ "already_in_progress": "La configuraci\u00f3 de la Samsung TV ja est\u00e0 en curs.", "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquesta Samsung TV.", "not_found": "No s'han trobat Samsung TV's compatibles a la xarxa.", + "not_successful": "No s'ha pogut connectar amb el dispositiu Samsung TV.", "not_supported": "Actualment aquest dispositiu Samsung TV no \u00e9s compatible." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Vols configurar la Samsung TV {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la TV demanant autenticaci\u00f3. Les configuracuons manuals d'aquesta TV es sobreescriuran.", diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json index 117069eb016..379fd5d8b6d 100644 --- a/homeassistant/components/samsungtv/.translations/da.json +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Dette Samsung-tv er allerede konfigureret.", "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", - "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.", + "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv. Tjek dit tvs indstillinger for at godkende Home Assistant.", "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", "not_successful": "Kan ikke oprette forbindelse til denne Samsung tv-enhed.", "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json index 60372837ffc..8760347e902 100644 --- a/homeassistant/components/samsungtv/.translations/de.json +++ b/homeassistant/components/samsungtv/.translations/de.json @@ -5,8 +5,10 @@ "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", "auth_missing": "Home Assistant ist nicht authentifiziert, um eine Verbindung zu diesem Samsung TV herzustellen.", "not_found": "Keine unterst\u00fctzten Samsung TV-Ger\u00e4te im Netzwerk gefunden.", + "not_successful": "Es kann keine Verbindung zu diesem Samsung-Fernsehger\u00e4t hergestellt werden.", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", diff --git a/homeassistant/components/samsungtv/.translations/fr.json b/homeassistant/components/samsungtv/.translations/fr.json index b880e41e5df..e381660a3e2 100644 --- a/homeassistant/components/samsungtv/.translations/fr.json +++ b/homeassistant/components/samsungtv/.translations/fr.json @@ -5,8 +5,10 @@ "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", "auth_missing": "Home Assistant n'est pas authentifi\u00e9 pour se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung.", "not_found": "Aucun t\u00e9l\u00e9viseur Samsung pris en charge trouv\u00e9 sur le r\u00e9seau.", + "not_successful": "Impossible de se connecter \u00e0 cet appareil Samsung TV.", "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json index c783db24720..3d2d4dd8e11 100644 --- a/homeassistant/components/samsungtv/.translations/it.json +++ b/homeassistant/components/samsungtv/.translations/it.json @@ -3,13 +3,15 @@ "abort": { "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.", "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.", - "auth_missing": "Home Assistant non \u00e8 autenticato per connettersi a questo Samsung TV.", + "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.", "not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.", + "not_successful": "Impossibile connettersi a questo dispositivo Samsung TV.", "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vuoi configurare Samsung TV {model} ? Se non hai mai collegato Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione. Le configurazioni manuali per questo televisore verranno sovrascritte.", + "description": "Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.", "title": "Samsung TV" }, "user": { @@ -17,7 +19,7 @@ "host": "Host o indirizzo IP", "name": "Nome" }, - "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione.", + "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul TV in cui \u00e8 richiesta l'autorizzazione.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/samsungtv/.translations/ko.json b/homeassistant/components/samsungtv/.translations/ko.json index f7656eb9035..0226fd52dc0 100644 --- a/homeassistant/components/samsungtv/.translations/ko.json +++ b/homeassistant/components/samsungtv/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", - "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", "not_found": "\uc9c0\uc6d0\ub418\ub294 \uc0bc\uc131 TV \ubaa8\ub378\uc774 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "not_successful": "\uc0bc\uc131 TV \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/sensor/.translations/sv.json b/homeassistant/components/sensor/.translations/sv.json new file mode 100644 index 00000000000..90001148f12 --- /dev/null +++ b/homeassistant/components/sensor/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Aktuell {entity_name} batteriniv\u00e5", + "is_humidity": "Aktuell {entity_name} fuktighet", + "is_illuminance": "Aktuell {entity_name} belysning", + "is_power": "Aktuell {entity_name} str\u00f6m", + "is_pressure": "Aktuellt {entity_name} tryck", + "is_signal_strength": "Aktuell {entity_name} signalstyrka", + "is_temperature": "Aktuell {entity_name} temperatur", + "is_timestamp": "Aktuell {entity_name} tidsst\u00e4mpel", + "is_value": "Aktuellt {entity_name} v\u00e4rde" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", + "humidity": "{entity_name} fuktighet \u00e4ndras", + "illuminance": "{entity_name} belysning \u00e4ndras", + "power": "{entity_name} str\u00f6mf\u00f6r\u00e4ndringar", + "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", + "signal_strength": "{entity_name} signalstyrka \u00e4ndras", + "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", + "timestamp": "{entity_name} tidst\u00e4mpel \u00e4ndras", + "value": "{entity_name} v\u00e4rde \u00e4ndras" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/sv.json b/homeassistant/components/sentry/.translations/sv.json new file mode 100644 index 00000000000..7f0968e7dbe --- /dev/null +++ b/homeassistant/components/sentry/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry har redan konfigurerats" + }, + "error": { + "bad_dsn": "Ogiltig DSN", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "description": "Ange din Sentry DSN", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/sv.json b/homeassistant/components/solaredge/.translations/sv.json new file mode 100644 index 00000000000..25bb0f325a1 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Denna site_id \u00e4r redan konfigurerad" + }, + "error": { + "site_exists": "Denna site_id \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckeln f\u00f6r den h\u00e4r webbplatsen", + "name": "Namnet p\u00e5 den h\u00e4r installationen", + "site_id": "SolarEdge webbplats-id" + }, + "title": "Definiera API-parametrarna f\u00f6r den h\u00e4r installationen" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/sv.json b/homeassistant/components/solarlog/.translations/sv.json new file mode 100644 index 00000000000..981bd9fb167 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta, kontrollera v\u00e4rdadressen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rdnamnet eller ip-adressen f\u00f6r din Solar-Log-enhet", + "name": "Prefixet som ska anv\u00e4ndas f\u00f6r dina Solar-Log sensorer" + }, + "title": "Definiera din Solar-Log-anslutning" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sv.json b/homeassistant/components/soma/.translations/sv.json new file mode 100644 index 00000000000..bb3ce895fd5 --- /dev/null +++ b/homeassistant/components/soma/.translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bara konfigurera ett Soma-konto.", + "authorize_url_timeout": "Timeout vid generering av auktoriserings-url.", + "connection_error": "Det gick inte att ansluta till SOMA Connect.", + "missing_configuration": "Soma-komponenten \u00e4r inte konfigurerad. F\u00f6lj dokumentationen.", + "result_error": "SOMA Connect svarade med felstatus." + }, + "create_entry": { + "default": "Lyckad autentisering med Soma." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange anslutningsinst\u00e4llningar f\u00f6r din SOMA Connect.", + "title": "SOMA Connect" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/sv.json b/homeassistant/components/somfy/.translations/sv.json index 390cd1f4d80..982b32a90a1 100644 --- a/homeassistant/components/somfy/.translations/sv.json +++ b/homeassistant/components/somfy/.translations/sv.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Lyckad autentisering med Somfy." }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sv.json b/homeassistant/components/starline/.translations/sv.json index 42d01b56753..83f2300892d 100644 --- a/homeassistant/components/starline/.translations/sv.json +++ b/homeassistant/components/starline/.translations/sv.json @@ -1,11 +1,42 @@ { "config": { + "error": { + "error_auth_app": "Fel applikations-id eller hemlighet", + "error_auth_mfa": "Felaktig kod", + "error_auth_user": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord" + }, "step": { "auth_app": { "data": { + "app_id": "App-ID", "app_secret": "Hemlighet" - } + }, + "description": "Applikations-ID och hemlig kod fr\u00e5n StarLine-utvecklarkonto", + "title": "Autentiseringsuppgifter f\u00f6r applikation" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kod fr\u00e5n bild" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kod" + }, + "description": "Ange koden som skickas till telefonen {phone_number}", + "title": "Tv\u00e5faktorautentisering" + }, + "auth_user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "StarLine-kontots e-postadress och l\u00f6senord", + "title": "Anv\u00e4ndaruppgifter" } - } + }, + "title": "StarLine" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sv.json b/homeassistant/components/switch/.translations/sv.json new file mode 100644 index 00000000000..3ec36265e52 --- /dev/null +++ b/homeassistant/components/switch/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "V\u00e4xla {entity_name}", + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5", + "turn_off": "{entity_name} st\u00e4ngdes av", + "turn_on": "{entity_name} slogs p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/sv.json b/homeassistant/components/tesla/.translations/sv.json new file mode 100644 index 00000000000..46263ff64ae --- /dev/null +++ b/homeassistant/components/tesla/.translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fel vid anslutning; kontrollera n\u00e4tverket och f\u00f6rs\u00f6k igen", + "identifier_exists": "E-post redan registrerad", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter", + "unknown_error": "Ok\u00e4nt fel, var god att rapportera logginformation" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "description": "V\u00e4nligen ange din information.", + "title": "Tesla - Konfiguration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellan skanningar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/sv.json b/homeassistant/components/traccar/.translations/sv.json new file mode 100644 index 00000000000..ddd33235e01 --- /dev/null +++ b/homeassistant/components/traccar/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot meddelanden fr\u00e5n Traccar.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du st\u00e4lla in webhook-funktionen i Traccar.\n\nAnv\u00e4nd f\u00f6ljande url: `{webhook_url}`\n\nMer information finns i [dokumentationen]({docs_url})." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill st\u00e4lla in Traccar?", + "title": "St\u00e4ll in Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json index 30004af17db..b2a00771e85 100644 --- a/homeassistant/components/transmission/.translations/sv.json +++ b/homeassistant/components/transmission/.translations/sv.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.", "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." }, "error": { "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "name_exists": "Namnet finns redan", "wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord" }, "step": { @@ -21,16 +23,20 @@ "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "St\u00e4ll in Transmission-klienten" } - } + }, + "title": "Transmission" }, "options": { "step": { "init": { "data": { "scan_interval": "Uppdateringsfrekvens" - } + }, + "description": "Konfigurera alternativ f\u00f6r Transmission", + "title": "Konfigurera alternativ f\u00f6r Transmission" } } } diff --git a/homeassistant/components/twentemilieu/.translations/sv.json b/homeassistant/components/twentemilieu/.translations/sv.json new file mode 100644 index 00000000000..ba2d8743681 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adressen har redan st\u00e4llts in." + }, + "error": { + "connection_error": "Det gick inte att ansluta.", + "invalid_address": "Adress hittades inte i serviceomr\u00e5det Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Husbrev/till\u00e4gg", + "house_number": "Husnummer", + "post_code": "Postnummer" + }, + "description": "St\u00e4ll in Twente Milieu som ger information om avfallshantering p\u00e5 din adress.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/sv.json b/homeassistant/components/unifi/.translations/sv.json index bc1d9f8cb72..23960999244 100644 --- a/homeassistant/components/unifi/.translations/sv.json +++ b/homeassistant/components/unifi/.translations/sv.json @@ -25,10 +25,23 @@ }, "options": { "step": { + "device_tracker": { + "data": { + "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", + "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", + "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", + "track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter" + } + }, "init": { "data": { "one": "Tom", - "other": "Tomma" + "other": "Tom" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" } } } diff --git a/homeassistant/components/vacuum/.translations/sv.json b/homeassistant/components/vacuum/.translations/sv.json new file mode 100644 index 00000000000..38b7f72ab9b --- /dev/null +++ b/homeassistant/components/vacuum/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "L\u00e5t {entity_name} st\u00e4da", + "dock": "L\u00e5t {entity_name} \u00e5terg\u00e5 till dockan" + }, + "condition_type": { + "is_cleaning": "{entity_name} st\u00e4dar", + "is_docked": "{entity_name} \u00e4r dockad" + }, + "trigger_type": { + "cleaning": "{entity_name} b\u00f6rjade st\u00e4da", + "docked": "{entity_name} dockad" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/sv.json b/homeassistant/components/velbus/.translations/sv.json new file mode 100644 index 00000000000..5a864439423 --- /dev/null +++ b/homeassistant/components/velbus/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Den h\u00e4r porten \u00e4r redan konfigurerad" + }, + "error": { + "connection_failed": "Velbus-anslutningen misslyckades", + "port_exists": "Den h\u00e4r porten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "name": "Namnet p\u00e5 den h\u00e4r velbus-anslutningen", + "port": "Anslutningsstr\u00e4ng" + }, + "title": "Definiera velbus-anslutningstypen" + } + }, + "title": "Velbus-gr\u00e4nssnitt" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/sv.json b/homeassistant/components/vesync/.translations/sv.json new file mode 100644 index 00000000000..a477ca6e5da --- /dev/null +++ b/homeassistant/components/vesync/.translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Endast en Vesync-instans \u00e4r till\u00e5ten" + }, + "error": { + "invalid_login": "Ogiltigt anv\u00e4ndarnamn eller l\u00f6senord" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Ange anv\u00e4ndarnamn och l\u00f6senord" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index 6901ffc1736..ed8a9386072 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Sembla que aquesta entrada ja s'ha configurat amb un amfitri\u00f3 i nom diferents a partir del n\u00famero de s\u00e8rie. Elimina les entrades antigues de configuraction.yaml i del men\u00fa d'integracions abans de provar d'afegir el dispositiu novament.", "host_exists": "Ja existeix un component Vizio configurat amb el host.", "name_exists": "Ja existeix un component Vizio configurat amb el nom.", + "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", "updated_options": "Aquesta entrada ja s'ha configurat per\u00f2 les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", "updated_volume_step": "Aquesta entrada ja s'ha configurat per\u00f2 la mida de l'increment de volum definit a la configuraci\u00f3 no coincideix, en conseq\u00fc\u00e8ncia, s'ha actualitzat." }, diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index 78d2347bfac..a6ae77365f4 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Cette entr\u00e9e semble avoir d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e avec un h\u00f4te et un nom diff\u00e9rents en fonction de son num\u00e9ro de s\u00e9rie. Veuillez supprimer toutes les anciennes entr\u00e9es de votre configuration.yaml et du menu Int\u00e9grations avant de r\u00e9essayer d'ajouter ce p\u00e9riph\u00e9rique.", "host_exists": "Composant Vizio avec h\u00f4te d\u00e9j\u00e0 configur\u00e9.", "name_exists": "Composant Vizio dont le nom est d\u00e9j\u00e0 configur\u00e9.", + "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", "updated_options": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais les options d\u00e9finies dans la configuration ne correspondent pas aux valeurs des options pr\u00e9c\u00e9demment import\u00e9es, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", "updated_volume_step": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e, mais la taille du pas du volume dans la configuration ne correspond pas \u00e0 l'entr\u00e9e de configuration, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." }, diff --git a/homeassistant/components/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index f3cb7b83026..dd27133453e 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Sembra che questa voce sia gi\u00e0 stata configurata con un host e un nome diversi in base al suo numero seriale. Rimuovere eventuali voci precedenti da configuration.yaml e dal menu Integrazioni prima di tentare nuovamente di aggiungere questo dispositivo.", "host_exists": "Componente Vizio con host gi\u00e0 configurato.", "name_exists": "Componente Vizio con nome gi\u00e0 configurato.", + "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza.", "updated_options": "Questa voce \u00e8 gi\u00e0 stata impostata, ma le opzioni definite nella configurazione non corrispondono ai valori delle opzioni importate in precedenza, quindi la voce di configurazione \u00e8 stata aggiornata di conseguenza.", "updated_volume_step": "Questa voce \u00e8 gi\u00e0 stata impostata, ma la dimensione del passo del volume nella configurazione non corrisponde alla voce di configurazione, quindi \u00e8 stata aggiornata di conseguenza." }, diff --git a/homeassistant/components/vizio/.translations/sv.json b/homeassistant/components/vizio/.translations/sv.json index 2c127f602ce..70d163e166f 100644 --- a/homeassistant/components/vizio/.translations/sv.json +++ b/homeassistant/components/vizio/.translations/sv.json @@ -1,31 +1,44 @@ { "config": { "abort": { - "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad." + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r vizio-komponenten p\u00e5g\u00e5r\nredan.", + "already_setup": "Den h\u00e4r posten har redan st\u00e4llts in.", + "already_setup_with_diff_host_and_name": "Den h\u00e4r posten verkar redan ha st\u00e4llts in med en annan v\u00e4rd och ett annat namn baserat p\u00e5 dess serienummer. Ta bort alla gamla poster fr\u00e5n configuration.yaml och fr\u00e5n menyn Integrationer innan du f\u00f6rs\u00f6ker l\u00e4gga till den h\u00e4r enheten igen.", + "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad.", + "name_exists": "Vizio-komponent med namn redan konfigurerad.", + "updated_entry": "Den h\u00e4r posten har redan konfigurerats men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen s\u00e5 konfigureringsposten har uppdaterats i enlighet med detta.", + "updated_options": "Den h\u00e4r posten har redan st\u00e4llts in men de alternativ som definierats i konfigurationen matchar inte de tidigare importerade alternativv\u00e4rdena s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta.", + "updated_volume_step": "Den h\u00e4r posten har redan st\u00e4llts in men volymstegstorleken i konfigurationen matchar inte konfigurationsposten s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta." }, "error": { + "cant_connect": "Det gick inte att ansluta till enheten. [Granska dokumentationen] (https://www.home-assistant.io/integrations/vizio/) och p\u00e5 nytt kontrollera att\n- Enheten \u00e4r p\u00e5slagen\n- Enheten \u00e4r ansluten till n\u00e4tverket\n- De v\u00e4rden du fyllt i \u00e4r korrekta\ninnan du f\u00f6rs\u00f6ker skicka in igen.", "host_exists": "Vizio-enheten med angivet v\u00e4rdnamn \u00e4r redan konfigurerad.", - "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad." + "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad.", + "tv_needs_token": "N\u00e4r Enhetstyp \u00e4r 'tv' beh\u00f6vs en giltig \u00e5tkomsttoken." }, "step": { "user": { "data": { "access_token": "\u00c5tkomstnyckel", "device_class": "Enhetstyp", + "host": ":", "name": "Namn" }, "title": "St\u00e4ll in Vizio SmartCast-klient" } }, - "title": "" + "title": "Vizio SmartCast" }, "options": { "step": { "init": { "data": { - "timeout": "Timeout f\u00f6r API-anrop (sekunder)" - } + "timeout": "Timeout f\u00f6r API-anrop (sekunder)", + "volume_step": "Storlek p\u00e5 volymsteg" + }, + "title": "Uppdatera Vizo SmartCast-alternativ" } - } + }, + "title": "Uppdatera Vizo SmartCast-alternativ" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index bd0ec740421..0ad55e7eaa7 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation.", "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." }, "create_entry": { diff --git a/homeassistant/components/withings/.translations/sv.json b/homeassistant/components/withings/.translations/sv.json index e2493e9afa7..dc8954af2c7 100644 --- a/homeassistant/components/withings/.translations/sv.json +++ b/homeassistant/components/withings/.translations/sv.json @@ -2,12 +2,31 @@ "config": { "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_flows": "Du m\u00e5ste konfigurera Withings innan du kan autentisera med den. L\u00e4s dokumentationen." + }, + "create_entry": { + "default": "Lyckad autentisering med Withings." }, "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" + }, + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", + "title": "Anv\u00e4ndarprofil." + }, + "user": { + "data": { + "profile": "Profil" + }, + "description": "V\u00e4lj en anv\u00e4ndarprofil som du vill att Home Assistant ska kartl\u00e4gga med en Withings-profil. Var noga med att v\u00e4lja samma anv\u00e4ndare p\u00e5 visningssidan eller s\u00e5 kommer inte data att betecknas korrekt.", + "title": "Anv\u00e4ndarprofil." } - } + }, + "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/sv.json b/homeassistant/components/wled/.translations/sv.json new file mode 100644 index 00000000000..980c023118e --- /dev/null +++ b/homeassistant/components/wled/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r WLED-enheten \u00e4r redan konfigurerad.", + "connection_error": "Det gick inte att ansluta till WLED-enheten." + }, + "error": { + "connection_error": "Det gick inte att ansluta till WLED-enheten." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress" + }, + "description": "St\u00e4ll in din WLED f\u00f6r att integrera med Home Assistant.", + "title": "L\u00e4nka din WLED" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till WLED med namnet `{name}` till Home Assistant?", + "title": "Uppt\u00e4ckt WLED-enhet" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json index 2762adc0fba..473cf1cd2a9 100644 --- a/homeassistant/components/zha/.translations/sv.json +++ b/homeassistant/components/zha/.translations/sv.json @@ -18,15 +18,50 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Kraxa", + "warn": "Varna" + }, "trigger_subtype": { + "both_buttons": "B\u00e5da knapparna", + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "button_5": "Femte knappen", + "button_6": "Sj\u00e4tte knappen", "close": "St\u00e4ng", "dim_down": "Dimma ned", "dim_up": "Dimma upp", + "face_1": "med bildsida 1 aktiverat", + "face_2": "med bildsida 2 aktiverat", + "face_3": "med bildsida 3 aktiverat", + "face_4": "med bildsida 4 aktiverat", + "face_5": "med bildsida 5 aktiverat", + "face_6": "med bildsida 6 aktiverat", + "face_any": "Med valfri/specificerad bildsida(or) aktiverat", "left": "V\u00e4nster", "open": "\u00d6ppen", "right": "H\u00f6ger", "turn_off": "St\u00e4ng av", "turn_on": "Starta" + }, + "trigger_type": { + "device_dropped": "Enheten tappades", + "device_flipped": "Enheten v\u00e4nd \"{subtype}\"", + "device_knocked": "Enheten knackad \"{subtype}\"", + "device_rotated": "Enheten roterade \"{subtype}\"", + "device_shaken": "Enheten skakad", + "device_slid": "Enheten gled \"{subtype}\"", + "device_tilted": "Enheten lutad", + "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", + "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", + "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", + "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", + "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", + "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", + "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickades" } } } \ No newline at end of file From 37205f9f6117366e86a1f0db2c5c4c83e5184d9d Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Fri, 7 Feb 2020 11:06:46 +0100 Subject: [PATCH 146/378] Unregister listener for stats sensor with max_age (#31580) --- homeassistant/components/statistics/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 865fda93a3e..d85b6b079ae 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -294,6 +294,7 @@ class StatisticsSensor(Entity): """Timer callback for sensor update.""" _LOGGER.debug("%s: executing scheduled update", self.entity_id) self.async_schedule_update_ha_state(True) + self._update_listener = None self._update_listener = async_track_point_in_utc_time( self.hass, _scheduled_update, next_to_purge_timestamp From c0eb399d5493e4a460eabe78fda411c679c2c603 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 7 Feb 2020 15:03:32 +0200 Subject: [PATCH 147/378] Fix librouteros response error handling (#31588) --- homeassistant/components/mikrotik/hub.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 2a503651d4b..f3423eea29c 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -126,7 +126,7 @@ class MikrotikData: def get_info(self, param): """Return device model name.""" cmd = IDENTITY if param == NAME else INFO - data = list(self.command(MIKROTIK_SERVICES[cmd])) + data = self.command(MIKROTIK_SERVICES[cmd]) return data[0].get(param) if data else None def get_hub_details(self): @@ -148,7 +148,7 @@ class MikrotikData: def get_list_from_interface(self, interface): """Get devices from interface.""" - result = list(self.command(MIKROTIK_SERVICES[interface])) + result = self.command(MIKROTIK_SERVICES[interface]) return self.load_mac(result) if result else {} def restore_device(self, mac): @@ -224,7 +224,7 @@ class MikrotikData: "address": ip_address, } cmd = "/ping" - data = list(self.command(cmd, params)) + data = self.command(cmd, params) if data is not None: status = 0 for result in data: @@ -242,9 +242,9 @@ class MikrotikData: try: _LOGGER.info("Running command %s", cmd) if params: - response = self.api(cmd=cmd, **params) + response = list(self.api(cmd=cmd, **params)) else: - response = self.api(cmd=cmd) + response = list(self.api(cmd=cmd)) except ( librouteros.exceptions.ConnectionClosed, socket.error, From f9e037a8231d994a84138c6c87e9c8f181515664 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Fri, 7 Feb 2020 09:16:56 -0500 Subject: [PATCH 148/378] update pynws to 0.10.4 (#31591) --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 5bb4cb46ee0..2bb77c2d95b 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.10.1"] + "requirements": ["pynws==0.10.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a5f4ec023b8..d83f0884def 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pynuki==1.3.3 pynut2==2.1.2 # homeassistant.components.nws -pynws==0.10.1 +pynws==0.10.4 # homeassistant.components.nx584 pynx584==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0be5b9c88c4..c8d873b2fd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -493,7 +493,7 @@ pymodbus==1.5.2 pymonoprice==0.3 # homeassistant.components.nws -pynws==0.10.1 +pynws==0.10.4 # homeassistant.components.nx584 pynx584==0.4 From 5483de7e252caac73953510cede221825c520370 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 8 Feb 2020 00:31:45 +0000 Subject: [PATCH 149/378] [ci skip] Translation update --- .../components/abode/.translations/hu.json | 22 ++++++ .../components/airly/.translations/hu.json | 25 ++++++ .../components/airly/.translations/nl.json | 3 + .../components/almond/.translations/hu.json | 19 +++++ .../components/almond/.translations/nl.json | 4 + .../components/axis/.translations/hu.json | 4 + .../components/axis/.translations/nl.json | 3 +- .../components/brother/.translations/hu.json | 17 +++++ .../components/brother/.translations/nl.json | 20 ++++- .../components/climate/.translations/hu.json | 17 +++++ .../components/deconz/.translations/hu.json | 76 ++++++++++++++++++- .../components/deconz/.translations/nl.json | 8 +- .../device_tracker/.translations/hu.json | 8 ++ .../components/ecobee/.translations/hu.json | 25 ++++++ .../components/elgato/.translations/hu.json | 15 ++++ .../garmin_connect/.translations/hu.json | 19 ++++- .../garmin_connect/.translations/nl.json | 24 ++++++ .../components/gdacs/.translations/es.json | 16 ++++ .../components/gdacs/.translations/fr.json | 16 ++++ .../components/gdacs/.translations/hu.json | 16 ++++ .../components/gdacs/.translations/lb.json | 16 ++++ .../components/gdacs/.translations/nl.json | 16 ++++ .../geonetnz_volcano/.translations/hu.json | 9 ++- .../components/gios/.translations/hu.json | 23 ++++++ .../components/gios/.translations/nl.json | 23 ++++++ .../hisense_aehw4a1/.translations/hu.json | 15 ++++ .../homekit_controller/.translations/hu.json | 33 +++++++- .../huawei_lte/.translations/hu.json | 35 +++++++++ .../iaqualink/.translations/hu.json | 11 +++ .../components/icloud/.translations/hu.json | 34 +++++++++ .../components/izone/.translations/hu.json | 11 +++ .../components/life360/.translations/hu.json | 8 ++ .../components/linky/.translations/hu.json | 12 +++ .../components/linky/.translations/nl.json | 3 + .../components/local_ip/.translations/hu.json | 16 ++++ .../components/local_ip/.translations/nl.json | 3 + .../lutron_caseta/.translations/hu.json | 5 ++ .../media_player/.translations/hu.json | 11 +++ .../meteo_france/.translations/es.json | 18 +++++ .../meteo_france/.translations/hu.json | 18 +++++ .../meteo_france/.translations/nl.json | 18 +++++ .../components/mikrotik/.translations/hu.json | 37 +++++++++ .../components/mikrotik/.translations/nl.json | 37 +++++++++ .../components/neato/.translations/hu.json | 27 +++++++ .../components/netatmo/.translations/hu.json | 17 +++++ .../components/netatmo/.translations/nl.json | 1 + .../opentherm_gw/.translations/hu.json | 33 ++++++++ .../components/plex/.translations/hu.json | 57 ++++++++++++++ .../components/ring/.translations/hu.json | 27 +++++++ .../components/ring/.translations/nl.json | 3 + .../samsungtv/.translations/es.json | 2 + .../samsungtv/.translations/hu.json | 16 +++- .../samsungtv/.translations/nl.json | 6 ++ .../components/sentry/.translations/af.json | 10 +++ .../components/sentry/.translations/hu.json | 18 +++++ .../components/sentry/.translations/nl.json | 13 +++- .../solaredge/.translations/hu.json | 13 ++++ .../components/solarlog/.translations/hu.json | 7 ++ .../components/soma/.translations/hu.json | 23 ++++++ .../components/somfy/.translations/hu.json | 9 +++ .../components/spotify/.translations/nl.json | 18 +++++ .../components/starline/.translations/hu.json | 32 +++++++- .../transmission/.translations/hu.json | 30 ++++++++ .../components/unifi/.translations/hu.json | 9 +++ .../components/vizio/.translations/es.json | 1 + .../components/vizio/.translations/hu.json | 43 +++++++++++ .../components/vizio/.translations/nl.json | 44 +++++++++++ .../components/withings/.translations/hu.json | 23 +++++- .../components/withings/.translations/nl.json | 5 ++ .../components/wled/.translations/hu.json | 26 +++++++ 70 files changed, 1263 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/abode/.translations/hu.json create mode 100644 homeassistant/components/airly/.translations/hu.json create mode 100644 homeassistant/components/almond/.translations/hu.json create mode 100644 homeassistant/components/climate/.translations/hu.json create mode 100644 homeassistant/components/device_tracker/.translations/hu.json create mode 100644 homeassistant/components/ecobee/.translations/hu.json create mode 100644 homeassistant/components/elgato/.translations/hu.json create mode 100644 homeassistant/components/garmin_connect/.translations/nl.json create mode 100644 homeassistant/components/gdacs/.translations/es.json create mode 100644 homeassistant/components/gdacs/.translations/fr.json create mode 100644 homeassistant/components/gdacs/.translations/hu.json create mode 100644 homeassistant/components/gdacs/.translations/lb.json create mode 100644 homeassistant/components/gdacs/.translations/nl.json create mode 100644 homeassistant/components/gios/.translations/hu.json create mode 100644 homeassistant/components/gios/.translations/nl.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/hu.json create mode 100644 homeassistant/components/huawei_lte/.translations/hu.json create mode 100644 homeassistant/components/iaqualink/.translations/hu.json create mode 100644 homeassistant/components/icloud/.translations/hu.json create mode 100644 homeassistant/components/izone/.translations/hu.json create mode 100644 homeassistant/components/local_ip/.translations/hu.json create mode 100644 homeassistant/components/lutron_caseta/.translations/hu.json create mode 100644 homeassistant/components/media_player/.translations/hu.json create mode 100644 homeassistant/components/meteo_france/.translations/es.json create mode 100644 homeassistant/components/meteo_france/.translations/hu.json create mode 100644 homeassistant/components/meteo_france/.translations/nl.json create mode 100644 homeassistant/components/mikrotik/.translations/hu.json create mode 100644 homeassistant/components/mikrotik/.translations/nl.json create mode 100644 homeassistant/components/neato/.translations/hu.json create mode 100644 homeassistant/components/netatmo/.translations/hu.json create mode 100644 homeassistant/components/opentherm_gw/.translations/hu.json create mode 100644 homeassistant/components/plex/.translations/hu.json create mode 100644 homeassistant/components/ring/.translations/hu.json create mode 100644 homeassistant/components/sentry/.translations/af.json create mode 100644 homeassistant/components/sentry/.translations/hu.json create mode 100644 homeassistant/components/solaredge/.translations/hu.json create mode 100644 homeassistant/components/solarlog/.translations/hu.json create mode 100644 homeassistant/components/soma/.translations/hu.json create mode 100644 homeassistant/components/somfy/.translations/hu.json create mode 100644 homeassistant/components/spotify/.translations/nl.json create mode 100644 homeassistant/components/transmission/.translations/hu.json create mode 100644 homeassistant/components/vizio/.translations/hu.json create mode 100644 homeassistant/components/vizio/.translations/nl.json create mode 100644 homeassistant/components/wled/.translations/hu.json diff --git a/homeassistant/components/abode/.translations/hu.json b/homeassistant/components/abode/.translations/hu.json new file mode 100644 index 00000000000..385334c8549 --- /dev/null +++ b/homeassistant/components/abode/.translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett." + }, + "error": { + "connection_error": "Nem lehet csatlakozni az Abode-hez.", + "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/hu.json b/homeassistant/components/airly/.translations/hu.json new file mode 100644 index 00000000000..30898c61abb --- /dev/null +++ b/homeassistant/components/airly/.translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van." + }, + "error": { + "auth": "Az API kulcs nem megfelel\u0151.", + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "Az integr\u00e1ci\u00f3 neve" + }, + "description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json index 232d5d54d85..a9c6865ad91 100644 --- a/homeassistant/components/airly/.translations/nl.json +++ b/homeassistant/components/airly/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly-integratie voor deze co\u00f6rdinaten is al geconfigureerd." + }, "error": { "auth": "API-sleutel is niet correct.", "name_exists": "Naam bestaat al.", diff --git a/homeassistant/components/almond/.translations/hu.json b/homeassistant/components/almond/.translations/hu.json new file mode 100644 index 00000000000..2331e57c6eb --- /dev/null +++ b/homeassistant/components/almond/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_setup": "Csak egy Almond fi\u00f3kot konfigur\u00e1lhat.", + "cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.", + "missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t." + }, + "step": { + "hassio_confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", + "title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" + }, + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json index d77fe69f7fa..939a9a904ad 100644 --- a/homeassistant/components/almond/.translations/nl.json +++ b/homeassistant/components/almond/.translations/nl.json @@ -6,6 +6,10 @@ "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." }, "step": { + "hassio_confirm": { + "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de hass.io add-on {addon} ?", + "title": "Almond via Hass.io add-on" + }, "pick_implementation": { "title": "Kies de authenticatie methode" } diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json index 41dd3c00d2b..4f05087cad8 100644 --- a/homeassistant/components/axis/.translations/hu.json +++ b/homeassistant/components/axis/.translations/hu.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "updated_configuration": "Friss\u00edtett eszk\u00f6zkonfigur\u00e1ci\u00f3 \u00faj \u00e1llom\u00e1sc\u00edmmel" + }, "error": { "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", "device_unavailable": "Az eszk\u00f6z nem \u00e9rhet\u0151 el", "faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok" }, + "flow_title": "Axis eszk\u00f6z: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json index 10fc8c02d66..b512690e2a3 100644 --- a/homeassistant/components/axis/.translations/nl.json +++ b/homeassistant/components/axis/.translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "Apparaat is al geconfigureerd", "bad_config_file": "Slechte gegevens van het configuratiebestand", "link_local_address": "Link-lokale adressen worden niet ondersteund", - "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat" + "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat", + "updated_configuration": "Bijgewerkte apparaatconfiguratie met nieuw hostadres" }, "error": { "already_configured": "Apparaat is al geconfigureerd", diff --git a/homeassistant/components/brother/.translations/hu.json b/homeassistant/components/brother/.translations/hu.json index a0e83450b37..1907d65f289 100644 --- a/homeassistant/components/brother/.translations/hu.json +++ b/homeassistant/components/brother/.translations/hu.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "Ez a nyomtat\u00f3 m\u00e1r konfigur\u00e1lva van.", + "unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott." + }, + "error": { + "connection_error": "Csatlakoz\u00e1si hiba.", + "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." + }, "flow_title": "Brother nyomtat\u00f3: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Nyomtat\u00f3 \u00e1llom\u00e1sneve vagy IP-c\u00edme", + "type": "A nyomtat\u00f3 t\u00edpusa" + }, + "description": "A Brother nyomtat\u00f3 integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha probl\u00e9m\u00e1id vannak a konfigur\u00e1ci\u00f3val, l\u00e1togass el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/brother", + "title": "Brother nyomtat\u00f3" + }, "zeroconf_confirm": { "data": { "type": "A nyomtat\u00f3 t\u00edpusa" diff --git a/homeassistant/components/brother/.translations/nl.json b/homeassistant/components/brother/.translations/nl.json index ed7d3980f47..c72aab46801 100644 --- a/homeassistant/components/brother/.translations/nl.json +++ b/homeassistant/components/brother/.translations/nl.json @@ -1,18 +1,32 @@ { "config": { "abort": { + "already_configured": "Deze printer is al geconfigureerd.", "unsupported_model": "Dit printermodel wordt niet ondersteund." }, "error": { "connection_error": "Verbindingsfout.", + "snmp_error": "SNMP-server uitgeschakeld of printer wordt niet ondersteund.", "wrong_host": "Ongeldige hostnaam of IP-adres." }, + "flow_title": "Brother Printer: {model} {serial_number}", "step": { "user": { "data": { - "host": "Printerhostnaam of IP-adres" - } + "host": "Printerhostnaam of IP-adres", + "type": "Type printer" + }, + "description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother", + "title": "Brother Printer" + }, + "zeroconf_confirm": { + "data": { + "type": "Type printer" + }, + "description": "Wilt u het Brother Printer {model} met serienummer {serial_number}' toevoegen aan Home Assistant?", + "title": "Ontdekte Brother Printer" } - } + }, + "title": "Brother Printer" } } \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/hu.json b/homeassistant/components/climate/.translations/hu.json new file mode 100644 index 00000000000..38d6cc68822 --- /dev/null +++ b/homeassistant/components/climate/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "F\u0171t\u00e9s- \u00e9s l\u00e9gtechnikai (HVAC) \u00fczemm\u00f3d m\u00f3dos\u00edt\u00e1sa a k\u00f6vetkez\u0151n: {entity_name}", + "set_preset_mode": "A(z) {entity_name} be\u00e1ll\u00edt\u00e1s\u00e1nak v\u00e1lt\u00e1sa" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} speci\u00e1lis f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3dra van be\u00e1ll\u00edtva", + "is_preset_mode": "A(z) {entity_name} el\u0151re be\u00e1ll\u00edtott m\u00f3dja van kiv\u00e1lasztva" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e9rt p\u00e1ratartalma megv\u00e1ltozott", + "current_temperature_changed": "{entity_name} m\u00e9rt h\u0151m\u00e9rs\u00e9klete megv\u00e1ltozott", + "hvac_mode_changed": "{entity_name} f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3d megv\u00e1ltozott" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index f162130680c..c5bf9718127 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -2,13 +2,23 @@ "config": { "abort": { "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "Az \u00e1tj\u00e1r\u00f3 konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.", "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", - "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" + "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3", + "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat", + "updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve" }, "error": { "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" }, + "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})", "step": { + "hassio_confirm": { + "data": { + "allow_clip_sensor": "Virtu\u00e1lis \u00e9rz\u00e9kel\u0151k import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" + }, + "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" + }, "init": { "data": { "host": "Hoszt", @@ -32,12 +42,72 @@ }, "device_automation": { "trigger_subtype": { - "close": "Bez\u00e1r\u00e1s" + "both_buttons": "Mindk\u00e9t gomb", + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "close": "Bez\u00e1r\u00e1s", + "dim_down": "S\u00f6t\u00e9t\u00edt", + "dim_up": "Vil\u00e1gos\u00edt", + "left": "Balra", + "open": "Nyitva", + "right": "Jobbra", + "side_1": "1. oldal", + "side_2": "2. oldal", + "side_3": "3. oldal", + "side_4": "4. oldal", + "side_5": "5. oldal", + "side_6": "6. oldal", + "turn_off": "Kikapcsolva", + "turn_on": "Bekapcsolva" }, "trigger_type": { + "remote_awakened": "A k\u00e9sz\u00fcl\u00e9k fel\u00e9bredt", + "remote_button_double_press": "\" {subtype} \" gombra k\u00e9tszer kattintottak", + "remote_button_long_press": "A \" {subtype} \" gomb folyamatosan lenyomva", + "remote_button_long_release": "A \" {subtype} \" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_quadruple_press": "\" {subtype} \" gombra n\u00e9gyszer kattintottak", + "remote_button_quintuple_press": "\" {subtype} \" gombra \u00f6tsz\u00f6r kattintottak", + "remote_button_rotated": "A gomb elforgatva: \" {subtype} \"", + "remote_button_rotation_stopped": "A (z) \" {subtype} \" gomb forg\u00e1sa le\u00e1llt", + "remote_button_short_press": "\" {subtype} \" gomb lenyomva", + "remote_button_short_release": "\"{alt\u00edpus}\" gomb elengedve", + "remote_button_triple_press": "\" {subtype} \" gombra h\u00e1romszor kattintottak", + "remote_double_tap": "Az \" {subtype} \" eszk\u00f6z dupla kattint\u00e1sa", "remote_double_tap_any_side": "A k\u00e9sz\u00fcl\u00e9k b\u00e1rmelyik oldal\u00e1n dupl\u00e1n koppint.", + "remote_falling": "K\u00e9sz\u00fcl\u00e9k szabades\u00e9sben", "remote_flip_180_degrees": "180 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", - "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z" + "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", + "remote_gyro_activated": "A k\u00e9sz\u00fcl\u00e9k meg lett r\u00e1zva", + "remote_moved": "Az eszk\u00f6z a \" {subtype} \"-lal felfel\u00e9 mozgatva", + "remote_moved_any_side": "A k\u00e9sz\u00fcl\u00e9k valamelyik oldal\u00e1val felfel\u00e9 mozogott", + "remote_rotate_from_side_1": "Az eszk\u00f6z a \"1. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_5": "Az eszk\u00f6z a \"5. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_turned_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val megegyez\u0151en fordult", + "remote_turned_counter_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val ellent\u00e9tes ir\u00e1nyban fordult" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", + "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se" + }, + "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", + "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se" + }, + "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index c0ee391b0c7..585c09c5339 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", "remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt", + "remote_double_tap_any_side": "Apparaat dubbel getikt aan elke kant", "remote_falling": "Apparaat in vrije val", + "remote_flip_180_degrees": "Apparaat 180 graden omgedraaid", + "remote_flip_90_degrees": "Apparaat 90 graden omgedraaid", "remote_gyro_activated": "Apparaat geschud", "remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog", + "remote_moved_any_side": "Apparaat gedraaid met elke kant naar boven", "remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".", "remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".", "remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"", "remote_rotate_from_side_4": "Apparaat gedraaid van \"zijde 4\" naar \" {subtype} \"", "remote_rotate_from_side_5": "Apparaat gedraaid van \"zijde 5\" naar \" {subtype} \"", - "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"" + "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"", + "remote_turned_clockwise": "Apparaat met de klok mee gedraaid", + "remote_turned_counter_clockwise": "Apparaat tegen de klok in gedraaid" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/hu.json b/homeassistant/components/device_tracker/.translations/hu.json new file mode 100644 index 00000000000..7302f40df9e --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/hu.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} otthon van", + "is_not_home": "{entity_name} nincs otthon" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/hu.json b/homeassistant/components/ecobee/.translations/hu.json new file mode 100644 index 00000000000..0950d52bd0e --- /dev/null +++ b/homeassistant/components/ecobee/.translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ez az integr\u00e1ci\u00f3 jelenleg csak egy ecobee p\u00e9ld\u00e1nyt t\u00e1mogat." + }, + "error": { + "pin_request_failed": "Hiba t\u00f6rt\u00e9nt a PIN-k\u00f3d ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 k\u00e9r\u00e9sekor; ellen\u0151rizze, hogy az API-kulcs helyes-e.", + "token_request_failed": "Hiba t\u00f6rt\u00e9nt a tokenek ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 ig\u00e9nyl\u00e9se k\u00f6zben; pr\u00f3b\u00e1lkozzon \u00fajra." + }, + "step": { + "authorize": { + "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \n Ezut\u00e1n nyomja meg a K\u00fcld\u00e9s gombot.", + "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on" + }, + "user": { + "data": { + "api_key": "API kulcs" + }, + "description": "Adja meg az ecobee.com webhelyr\u0151l beszerzett API-kulcsot.", + "title": "ecobee API kulcs" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/hu.json b/homeassistant/components/elgato/.translations/hu.json new file mode 100644 index 00000000000..60b4e79b346 --- /dev/null +++ b/homeassistant/components/elgato/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "connection_error": "Nem siker\u00fclt csatlakozni az Elgato Key Light eszk\u00f6zh\u00f6z." + }, + "step": { + "user": { + "data": { + "host": "Hosztn\u00e9v vagy IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/hu.json b/homeassistant/components/garmin_connect/.translations/hu.json index de4dea29166..931fa295962 100644 --- a/homeassistant/components/garmin_connect/.translations/hu.json +++ b/homeassistant/components/garmin_connect/.translations/hu.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Ez a fi\u00f3k m\u00e1r konfigur\u00e1lva van." - } + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.", + "too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.", + "unknown": "V\u00e1ratlan hiba." + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg a hiteles\u00edt\u0151 adatait.", + "title": "Garmin Csatlakoz\u00e1s" + } + }, + "title": "Garmin Csatlakoz\u00e1s" } } \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/nl.json b/homeassistant/components/garmin_connect/.translations/nl.json new file mode 100644 index 00000000000..c7a690de6e2 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dit account is al geconfigureerd." + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw.", + "invalid_auth": "Ongeldige authenticatie", + "too_many_requests": "Te veel aanvragen, probeer het later opnieuw.", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer uw gegevens in", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/es.json b/homeassistant/components/gdacs/.translations/es.json new file mode 100644 index 00000000000..6c02b339541 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Rellena los datos de tu filtro." + } + }, + "title": "Sistema Mundial de Alerta y Coordinaci\u00f3n de Desastres (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/fr.json b/homeassistant/components/gdacs/.translations/fr.json new file mode 100644 index 00000000000..a4366cb5dc7 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9." + }, + "step": { + "user": { + "data": { + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "Syst\u00e8me mondial d'alerte et de coordination en cas de catastrophe (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/hu.json b/homeassistant/components/gdacs/.translations/hu.json new file mode 100644 index 00000000000..79bcba3388f --- /dev/null +++ b/homeassistant/components/gdacs/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van." + }, + "step": { + "user": { + "data": { + "radius": "Sug\u00e1r" + }, + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + } + }, + "title": "Glob\u00e1lis katasztr\u00f3fariaszt\u00e1si \u00e9s koordin\u00e1ci\u00f3s rendszer (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/lb.json b/homeassistant/components/gdacs/.translations/lb.json new file mode 100644 index 00000000000..a4077ed630e --- /dev/null +++ b/homeassistant/components/gdacs/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Standuert ass scho konfigu\u00e9iert." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "Globale D\u00e9saster Alerte a Koordinatioun System (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/nl.json b/homeassistant/components/gdacs/.translations/nl.json new file mode 100644 index 00000000000..62383e43e36 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Vul uw filtergegevens in." + } + }, + "title": "Wereldwijd rampenwaarschuwings- en co\u00f6rdinatiesysteem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/hu.json b/homeassistant/components/geonetnz_volcano/.translations/hu.json index 875a8330f76..e53a91bcb03 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/hu.json +++ b/homeassistant/components/geonetnz_volcano/.translations/hu.json @@ -1,9 +1,16 @@ { "config": { + "error": { + "identifier_exists": "A hely m\u00e1r regisztr\u00e1lt" + }, "step": { "user": { + "data": { + "radius": "Sug\u00e1r" + }, "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." } - } + }, + "title": "GeoNet NZ vulk\u00e1n" } } \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/hu.json b/homeassistant/components/gios/.translations/hu.json new file mode 100644 index 00000000000..75fcb2088a5 --- /dev/null +++ b/homeassistant/components/gios/.translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "A GIO\u015a integr\u00e1ci\u00f3 ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz m\u00e1r konfigur\u00e1lva van." + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni a GIO\u015a szerverhez.", + "invalid_sensors_data": "\u00c9rv\u00e9nytelen \u00e9rz\u00e9kel\u0151k adatai ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz.", + "wrong_station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja nem megfelel\u0151." + }, + "step": { + "user": { + "data": { + "name": "Az integr\u00e1ci\u00f3 neve", + "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja" + }, + "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Lengyel K\u00f6rnyezetv\u00e9delmi F\u0151fel\u00fcgyel\u0151s\u00e9g)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/nl.json b/homeassistant/components/gios/.translations/nl.json new file mode 100644 index 00000000000..eb487681838 --- /dev/null +++ b/homeassistant/components/gios/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a-integratie voor dit meetstation is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met de GIO\u015a-server.", + "invalid_sensors_data": "Ongeldige sensorgegevens voor dit meetstation.", + "wrong_station_id": "ID van het meetstation is niet correct." + }, + "step": { + "user": { + "data": { + "name": "Naam van de integratie", + "station_id": "ID van het meetstation" + }, + "description": "GIO\u015a (Poolse hoofdinspectie van milieubescherming) luchtkwaliteitintegratie instellen. Als u hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Poolse hoofdinspectie van milieubescherming)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/hu.json b/homeassistant/components/hisense_aehw4a1/.translations/hu.json new file mode 100644 index 00000000000..02716a96a73 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 Hisense AEH-W4A1 eszk\u00f6z.", + "single_instance_allowed": "Csak egy konfigur\u00e1ci\u00f3 lehet Hisense AEH-W4A1 eset\u00e9n." + }, + "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Hisense AEH-W4A1-et?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/hu.json b/homeassistant/components/homekit_controller/.translations/hu.json index 60bd173dc8e..264e635d7f4 100644 --- a/homeassistant/components/homekit_controller/.translations/hu.json +++ b/homeassistant/components/homekit_controller/.translations/hu.json @@ -1,11 +1,40 @@ { "config": { + "abort": { + "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.", + "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.", + "already_in_progress": "Az eszk\u00f6z konfigur\u00e1ci\u00f3ja m\u00e1r folyamatban van.", + "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", + "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", + "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", + "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" + }, + "error": { + "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "busy_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik vez\u00e9rl\u0151vel.", + "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs ingyenes p\u00e1ros\u00edt\u00e1si t\u00e1rhely.", + "max_tries_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel t\u00f6bb mint 100 sikertelen hiteles\u00edt\u00e9si k\u00eds\u00e9rletet kapott.", + "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy jelenleg nem t\u00e1mogatja az eszk\u00f6zt.", + "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", + "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen." + }, + "flow_title": "HomeKit tartoz\u00e9k: {name}", "step": { + "pair": { + "data": { + "pairing_code": "P\u00e1ros\u00edt\u00e1si k\u00f3d" + }, + "description": "\u00cdrja be a HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban) a kieg\u00e9sz\u00edt\u0151 haszn\u00e1lat\u00e1hoz", + "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" + }, "user": { "data": { "device": "Eszk\u00f6z" - } + }, + "description": "V\u00e1lassza ki azt az eszk\u00f6zt, amelyet p\u00e1ros\u00edtani szeretne", + "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" } - } + }, + "title": "HomeKit tartoz\u00e9k" } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/hu.json b/homeassistant/components/huawei_lte/.translations/hu.json new file mode 100644 index 00000000000..6c029d4fd5d --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/hu.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_in_progress": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" + }, + "error": { + "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9se", + "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", + "incorrect_username_or_password": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "url": "URL", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Huawei LTE konfigur\u00e1l\u00e1sa" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u00c9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s neve (a m\u00f3dos\u00edt\u00e1s \u00fajraind\u00edt\u00e1st ig\u00e9nyel)", + "recipient": "SMS-\u00e9rtes\u00edt\u00e9s c\u00edmzettjei", + "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomk\u00f6vet\u00e9se" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/hu.json b/homeassistant/components/iaqualink/.translations/hu.json new file mode 100644 index 00000000000..6f4976647d0 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Felhaszn\u00e1l\u00f3n\u00e9v / e-mail c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/hu.json b/homeassistant/components/icloud/.translations/hu.json new file mode 100644 index 00000000000..14c8c8e4e2f --- /dev/null +++ b/homeassistant/components/icloud/.translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "login": "Bejelentkez\u00e9si hiba: k\u00e9rj\u00fck, ellen\u0151rizze e-mail c\u00edm\u00e9t \u00e9s jelszav\u00e1t", + "send_verification_code": "Nem siker\u00fclt elk\u00fcldeni az ellen\u0151rz\u0151 k\u00f3dot", + "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": { + "trusted_device": { + "data": { + "trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a megb\u00edzhat\u00f3 eszk\u00f6zt", + "title": "iCloud megb\u00edzhat\u00f3 eszk\u00f6z" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + }, + "description": "Adja meg hiteles\u00edt\u0151 adatait", + "title": "iCloud hiteles\u00edt\u0151 adatok" + }, + "verification_code": { + "data": { + "verification_code": "Ellen\u0151rz\u0151 k\u00f3d" + }, + "description": "K\u00e9rj\u00fck, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", + "title": "iCloud ellen\u0151rz\u0151 k\u00f3d" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/hu.json b/homeassistant/components/izone/.translations/hu.json new file mode 100644 index 00000000000..79c621ce125 --- /dev/null +++ b/homeassistant/components/izone/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/hu.json b/homeassistant/components/life360/.translations/hu.json index 227e784b065..b43d0bfeff5 100644 --- a/homeassistant/components/life360/.translations/hu.json +++ b/homeassistant/components/life360/.translations/hu.json @@ -1,7 +1,15 @@ { "config": { "error": { + "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", "unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel" + }, + "step": { + "user": { + "data": { + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/hu.json b/homeassistant/components/linky/.translations/hu.json index 436e8b1fb7d..66314d14ee1 100644 --- a/homeassistant/components/linky/.translations/hu.json +++ b/homeassistant/components/linky/.translations/hu.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "access": "Nem siker\u00fclt el\u00e9rni az Enedis.fr webhelyet, ellen\u0151rizze internet-kapcsolat\u00e1t", + "enedis": "Az Enedis.fr hib\u00e1val v\u00e1laszolt: k\u00e9rj\u00fck, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 k\u00f6z\u00f6tt)", + "unknown": "Ismeretlen hiba: pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 \u00f3ra k\u00f6z\u00f6tt)" + }, + "step": { + "user": { + "data": { + "username": "E-mail" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json index ecc566c8b87..5654edb08f4 100644 --- a/homeassistant/components/linky/.translations/nl.json +++ b/homeassistant/components/linky/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account al geconfigureerd" + }, "error": { "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding", "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", diff --git a/homeassistant/components/local_ip/.translations/hu.json b/homeassistant/components/local_ip/.translations/hu.json new file mode 100644 index 00000000000..7a78029c379 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Az integr\u00e1ci\u00f3 m\u00e1r konfigur\u00e1lva van egy ilyen nev\u0171 l\u00e9tez\u0151 \u00e9rz\u00e9kel\u0151vel" + }, + "step": { + "user": { + "data": { + "name": "\u00c9rz\u00e9kel\u0151 neve" + }, + "title": "Helyi IP c\u00edm" + } + }, + "title": "Helyi IP c\u00edm" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/nl.json b/homeassistant/components/local_ip/.translations/nl.json index 4f0d9a437db..6f22d2c585a 100644 --- a/homeassistant/components/local_ip/.translations/nl.json +++ b/homeassistant/components/local_ip/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met die naam" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/.translations/hu.json b/homeassistant/components/lutron_caseta/.translations/hu.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/hu.json b/homeassistant/components/media_player/.translations/hu.json new file mode 100644 index 00000000000..fbefbc43e08 --- /dev/null +++ b/homeassistant/components/media_player/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} t\u00e9tlen", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "is_paused": "{entity_name} sz\u00fcneteltetve van", + "is_playing": "{entity_name} lej\u00e1tszik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/es.json b/homeassistant/components/meteo_france/.translations/es.json new file mode 100644 index 00000000000..3cd7ee56252 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "La ciudad ya est\u00e1 configurada", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde" + }, + "step": { + "user": { + "data": { + "city": "Ciudad" + }, + "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/hu.json b/homeassistant/components/meteo_france/.translations/hu.json new file mode 100644 index 00000000000..f1719f4bf30 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van", + "unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb" + }, + "step": { + "user": { + "data": { + "city": "V\u00e1ros" + }, + "description": "\u00cdrja be az ir\u00e1ny\u00edt\u00f3sz\u00e1mot (csak Franciaorsz\u00e1g eset\u00e9ben aj\u00e1nlott) vagy a v\u00e1ros nev\u00e9t", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/nl.json b/homeassistant/components/meteo_france/.translations/nl.json new file mode 100644 index 00000000000..648ef0c5fbd --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Stad al geconfigureerd", + "unknown": "Onbekende fout: probeer het later nog eens" + }, + "step": { + "user": { + "data": { + "city": "Stad" + }, + "description": "Vul de postcode (alleen voor Frankrijk, aanbevolen) of de plaatsnaam in", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/hu.json b/homeassistant/components/mikrotik/.translations/hu.json new file mode 100644 index 00000000000..8afbeb69925 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/hu.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "A Mikrotik m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "A kapcsolat sikertelen", + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL haszn\u00e1lata" + }, + "title": "Mikrotik \u00fatv\u00e1laszt\u00f3 be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP-ping enged\u00e9lyez\u00e9se", + "detection_time": "Otthoni intervallumk\u00e9nt vegye figyelembe", + "force_dhcp": "A szkennel\u00e9s k\u00e9nyszer\u00edt\u00e9se DHCP seg\u00edts\u00e9g\u00e9vel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/nl.json b/homeassistant/components/mikrotik/.translations/nl.json new file mode 100644 index 00000000000..d4996d492a5 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding niet geslaagd", + "name_exists": "Naam bestaat al", + "wrong_credentials": "Ongeldige inloggegevens" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam", + "verify_ssl": "Gebruik SSL" + }, + "title": "Mikrotik Router instellen" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP-ping inschakelen", + "detection_time": "Overweeg thuisinterval", + "force_dhcp": "Forceer scannen met DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/hu.json b/homeassistant/components/neato/.translations/hu.json new file mode 100644 index 00000000000..50fa4b5866f --- /dev/null +++ b/homeassistant/components/neato/.translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "M\u00e1r konfigur\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, + "create_entry": { + "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )." + }, + "error": { + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok", + "unexpected_error": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "vendor": "Sz\u00e1ll\u00edt\u00f3" + }, + "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} ).", + "title": "Neato Fi\u00f3kinform\u00e1ci\u00f3" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/hu.json b/homeassistant/components/netatmo/.translations/hu.json new file mode 100644 index 00000000000..9994e527f01 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Csak egy Netatmo-fi\u00f3kot \u00e1ll\u00edthatsz be.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + }, + "create_entry": { + "default": "A Netatmo sikeresen hiteles\u00edtett." + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/nl.json b/homeassistant/components/netatmo/.translations/nl.json index d9062850f2a..5f5fe375117 100644 --- a/homeassistant/components/netatmo/.translations/nl.json +++ b/homeassistant/components/netatmo/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Netatmo account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." }, diff --git a/homeassistant/components/opentherm_gw/.translations/hu.json b/homeassistant/components/opentherm_gw/.translations/hu.json new file mode 100644 index 00000000000..8a0780581fd --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "already_configured": "Az \u00e1tj\u00e1r\u00f3 m\u00e1r konfigur\u00e1lva van", + "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik", + "serial_error": "Hiba t\u00f6rt\u00e9nt az eszk\u00f6zh\u00f6z val\u00f3 csatlakoz\u00e1skor", + "timeout": "A csatlakoz\u00e1si k\u00eds\u00e9rletre sz\u00e1nt id\u0151 lej\u00e1rt" + }, + "step": { + "init": { + "data": { + "device": "El\u00e9r\u00e9si \u00fat vagy URL", + "floor_temperature": "Padl\u00f3 kl\u00edma h\u0151m\u00e9rs\u00e9klete", + "id": "ID", + "name": "N\u00e9v", + "precision": "Kl\u00edma h\u0151m\u00e9rs\u00e9klet pontoss\u00e1ga" + }, + "title": "OpenTherm \u00e1tj\u00e1r\u00f3" + } + }, + "title": "OpenTherm \u00e1tj\u00e1r\u00f3" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", + "precision": "Pontoss\u00e1g" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/hu.json b/homeassistant/components/plex/.translations/hu.json new file mode 100644 index 00000000000..4712fb37b55 --- /dev/null +++ b/homeassistant/components/plex/.translations/hu.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", + "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van", + "discovery_no_file": "Nem tal\u00e1lhat\u00f3 r\u00e9gi konfigur\u00e1ci\u00f3s f\u00e1jl", + "invalid_import": "Az import\u00e1lt konfigur\u00e1ci\u00f3 \u00e9rv\u00e9nytelen", + "non-interactive": "Nem interakt\u00edv import\u00e1l\u00e1s", + "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", + "unknown": "Ismeretlen okb\u00f3l nem siker\u00fclt" + }, + "error": { + "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen", + "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz", + "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" + }, + "step": { + "manual_setup": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "port": "Port" + } + }, + "select_server": { + "data": { + "server": "szerver" + }, + "description": "T\u00f6bb szerver el\u00e9rhet\u0151, v\u00e1lasszon egyet:", + "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa" + }, + "start_website_auth": { + "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen.", + "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" + }, + "user": { + "data": { + "token": "Plex token" + }, + "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen, vagy manu\u00e1lisan konfigur\u00e1lja a szervert.", + "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Az \u00f6sszes vez\u00e9rl\u0151 megjelen\u00edt\u00e9se", + "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t" + }, + "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/hu.json b/homeassistant/components/ring/.translations/hu.json new file mode 100644 index 00000000000..578399c8152 --- /dev/null +++ b/homeassistant/components/ring/.translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "2fa": { + "data": { + "2fa": "K\u00e9tfaktoros k\u00f3d" + }, + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Bejelentkez\u00e9s a Ring fi\u00f3kkal" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/nl.json b/homeassistant/components/ring/.translations/nl.json index 1bb012bd25e..70736b15a9c 100644 --- a/homeassistant/components/ring/.translations/nl.json +++ b/homeassistant/components/ring/.translations/nl.json @@ -9,6 +9,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "Twee-factor code" + }, "title": "Tweestapsverificatie" }, "user": { diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json index 3535d4bc65f..4466b329a2a 100644 --- a/homeassistant/components/samsungtv/.translations/es.json +++ b/homeassistant/components/samsungtv/.translations/es.json @@ -5,8 +5,10 @@ "already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.", "auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.", "not_found": "No se encontraron televisiones Samsung compatibles en la red.", + "not_successful": "No se puede conectar a este dispositivo Samsung TV.", "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." }, + "flow_title": "Televisor Samsung: {model}", "step": { "confirm": { "description": "\u00bfDesea configurar el televisor Samsung {model} ? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", diff --git a/homeassistant/components/samsungtv/.translations/hu.json b/homeassistant/components/samsungtv/.translations/hu.json index 6d816ecb95a..c7a046428bc 100644 --- a/homeassistant/components/samsungtv/.translations/hu.json +++ b/homeassistant/components/samsungtv/.translations/hu.json @@ -1,14 +1,28 @@ { "config": { "abort": { + "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\u0151rizze a TV-k\u00e9sz\u00fcl\u00e9k\u00e9ben a Home Assistant enged\u00e9lyez\u00e9si be\u00e1ll\u00edt\u00e1sait.", "not_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 t\u00e1mogatott Samsung TV-eszk\u00f6z.", + "not_successful": "Nem lehet csatlakozni ehhez a Samsung TV k\u00e9sz\u00fcl\u00e9khez.", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Be\u00e1ll\u00edtja a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha soha nem csatlakozott home assistant-hez ezel\u0151tt, meg kell jelennie egy felugr\u00f3 ablaknak a TV-ben, ahol hiteles\u00edt\u00e9st k\u00e9r. A tv-k\u00e9sz\u00fcl\u00e9k manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Hosztn\u00e9v vagy IP c\u00edm", + "name": "N\u00e9v" + }, + "description": "\u00cdrja be a Samsung TV adatait. Ha soha nem csatlakoztatta a Home Assistant alkalmaz\u00e1st ezel\u0151tt, l\u00e1tnia kell a t\u00e9v\u00e9ben egy felugr\u00f3 ablakot, amely enged\u00e9lyt k\u00e9r.", + "title": "Samsung TV" } - } + }, + "title": "Samsung TV" } } \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/nl.json b/homeassistant/components/samsungtv/.translations/nl.json index 93bb5953e31..bc52c8c71f7 100644 --- a/homeassistant/components/samsungtv/.translations/nl.json +++ b/homeassistant/components/samsungtv/.translations/nl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Deze Samsung TV is al geconfigureerd.", + "already_in_progress": "Samsung TV configuratie is al in uitvoering.", "auth_missing": "Home Assistant is niet geverifieerd om verbinding te maken met deze Samsung TV.", "not_found": "Geen ondersteunde Samsung TV-apparaten gevonden op het netwerk.", + "not_successful": "Niet in staat om verbinding te maken met dit Samsung TV toestel.", "not_supported": "Deze Samsung TV-apparaten wordt momenteel niet ondersteund." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { + "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", "title": "Samsung TV" }, "user": { @@ -14,6 +19,7 @@ "host": "Hostnaam of IP-adres", "name": "Naam" }, + "description": "Voer uw Samsung TV informatie in. Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/sentry/.translations/af.json b/homeassistant/components/sentry/.translations/af.json new file mode 100644 index 00000000000..61ef8f8d389 --- /dev/null +++ b/homeassistant/components/sentry/.translations/af.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/hu.json b/homeassistant/components/sentry/.translations/hu.json new file mode 100644 index 00000000000..64318828e6d --- /dev/null +++ b/homeassistant/components/sentry/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az Sentry m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "bad_dsn": "\u00c9rv\u00e9nytelen DSN", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "description": "Add meg a Sentry DSN-t", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/nl.json b/homeassistant/components/sentry/.translations/nl.json index 7e198e836d7..67bd1ea54e2 100644 --- a/homeassistant/components/sentry/.translations/nl.json +++ b/homeassistant/components/sentry/.translations/nl.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Sentry is al geconfigureerd" + }, "error": { + "bad_dsn": "Ongeldige DSN", "unknown": "Onverwachte fout" - } + }, + "step": { + "user": { + "description": "Voer uw Sentry DSN in", + "title": "Sentry" + } + }, + "title": "Sentry" } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/hu.json b/homeassistant/components/solaredge/.translations/hu.json new file mode 100644 index 00000000000..ae8f51983ea --- /dev/null +++ b/homeassistant/components/solaredge/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ennek az install\u00e1ci\u00f3nak a neve" + }, + "title": "Az API param\u00e9terek megad\u00e1sa ehhez a telep\u00edt\u00e9shez" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/hu.json b/homeassistant/components/solarlog/.translations/hu.json new file mode 100644 index 00000000000..e52cebefda6 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/hu.json b/homeassistant/components/soma/.translations/hu.json new file mode 100644 index 00000000000..797cfa1b2d8 --- /dev/null +++ b/homeassistant/components/soma/.translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.", + "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." + }, + "create_entry": { + "default": "Soma sikeresen hiteles\u00edtett." + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "port": "Port" + }, + "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", + "title": "SOMA csatlakoz\u00e1s" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/hu.json b/homeassistant/components/somfy/.translations/hu.json new file mode 100644 index 00000000000..3df2fb30477 --- /dev/null +++ b/homeassistant/components/somfy/.translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/nl.json b/homeassistant/components/spotify/.translations/nl.json new file mode 100644 index 00000000000..abe59854044 --- /dev/null +++ b/homeassistant/components/spotify/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Spotify-account configureren.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd met Spotify." + }, + "step": { + "pick_implementation": { + "title": "Kies Authenticatiemethode" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/hu.json b/homeassistant/components/starline/.translations/hu.json index c45d9ac871e..ccc5b7983d0 100644 --- a/homeassistant/components/starline/.translations/hu.json +++ b/homeassistant/components/starline/.translations/hu.json @@ -8,9 +8,35 @@ "step": { "auth_app": { "data": { - "app_id": "App ID" - } + "app_id": "App ID", + "app_secret": "Titok" + }, + "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a StarLine fejleszt\u0151i fi\u00f3kb\u00f3l ", + "title": "Alkalmaz\u00e1si hiteles\u00edt\u0151 adatok" + }, + "auth_captcha": { + "data": { + "captcha_code": "K\u00f3d a k\u00e9pr\u0151l" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS k\u00f3d" + }, + "description": "Adja meg a {phone_number} telefonra k\u00fcld\u00f6tt k\u00f3dot.", + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" + }, + "auth_user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "A StarLine fi\u00f3k e-mail c\u00edme \u00e9s jelszava", + "title": "Felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok" } - } + }, + "title": "Starline" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/hu.json b/homeassistant/components/transmission/.translations/hu.json new file mode 100644 index 00000000000..14bf5c28bdf --- /dev/null +++ b/homeassistant/components/transmission/.translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Csak egyetlen p\u00e9ld\u00e1nyra van sz\u00fcks\u00e9g." + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni az \u00e1llom\u00e1shoz", + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_credentials": "Rossz felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" + }, + "step": { + "options": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" + }, + "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" + }, + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "\u00c1tviteli \u00fcgyf\u00e9l be\u00e1ll\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json index b927e652ba7..f6919f985dc 100644 --- a/homeassistant/components/unifi/.translations/hu.json +++ b/homeassistant/components/unifi/.translations/hu.json @@ -21,5 +21,14 @@ } }, "title": "UniFi Vez\u00e9rl\u0151" + }, + "options": { + "step": { + "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" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json index 009f93a50c6..408d94825f1 100644 --- a/homeassistant/components/vizio/.translations/es.json +++ b/homeassistant/components/vizio/.translations/es.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Esta entrada parece haber sido ya configurada con un host y un nombre diferentes basados en su n\u00famero de serie. Elimine las entradas antiguas de su archivo configuration.yaml y del men\u00fa Integraciones antes de volver a intentar agregar este dispositivo.", "host_exists": "Host ya configurado del componente de Vizio", "name_exists": "Nombre ya configurado del componente de Vizio", + "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.", "updated_options": "Esta entrada ya ha sido configurada pero las opciones definidas en la configuraci\u00f3n no coinciden con los valores de las opciones importadas previamente, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.", "updated_volume_step": "Esta entrada ya ha sido configurada pero el tama\u00f1o del paso de volumen en la configuraci\u00f3n no coincide con la entrada de la configuraci\u00f3n, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." }, diff --git a/homeassistant/components/vizio/.translations/hu.json b/homeassistant/components/vizio/.translations/hu.json new file mode 100644 index 00000000000..650d5133dbd --- /dev/null +++ b/homeassistant/components/vizio/.translations/hu.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "A vizio komponens konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.", + "already_setup": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva.", + "already_setup_with_diff_host_and_name": "\u00dagy t\u0171nik, hogy ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva egy m\u00e1sik \u00e1llom\u00e1ssal \u00e9s n\u00e9vvel a sorozatsz\u00e1ma alapj\u00e1n. T\u00e1vol\u00edtsa el a r\u00e9gi bejegyz\u00e9seket a configuration.yaml \u00e9s az Integr\u00e1ci\u00f3k men\u00fcb\u0151l, miel\u0151tt \u00fajra megpr\u00f3b\u00e1ln\u00e1 hozz\u00e1adni ezt az eszk\u00f6zt.", + "host_exists": "Vizio-\u00f6sszetev\u0151, amelynek az kiszolg\u00e1l\u00f3neve m\u00e1r konfigur\u00e1lva van.", + "name_exists": "Vizio-\u00f6sszetev\u0151, amelynek neve m\u00e1r konfigur\u00e1lva van.", + "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.", + "updated_options": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban megadott be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt be\u00e1ll\u00edt\u00e1si \u00e9rt\u00e9kekkel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.", + "updated_volume_step": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151 henger\u0151l\u00e9p\u00e9s m\u00e9rete nem egyezik meg a konfigur\u00e1ci\u00f3s bejegyz\u00e9ssel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." + }, + "error": { + "cant_connect": "Nem lehetett csatlakozni az eszk\u00f6zh\u00f6z. [Tekintsd \u00e1t a dokumentumokat] (https://www.home-assistant.io/integrations/vizio/) \u00e9s \u00fajra ellen\u0151rizd, hogy:\n- A k\u00e9sz\u00fcl\u00e9k be van kapcsolva\n- A k\u00e9sz\u00fcl\u00e9k csatlakozik a h\u00e1l\u00f3zathoz\n- A kit\u00f6lt\u00f6tt \u00e9rt\u00e9kek pontosak\nmiel\u0151tt \u00fajra elk\u00fclden\u00e9d.", + "host_exists": "A megadott kiszolg\u00e1l\u00f3n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "name_exists": "A megadott n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "tv_needs_token": "Ha az eszk\u00f6z t\u00edpusa \"tv\", akkor \u00e9rv\u00e9nyes hozz\u00e1f\u00e9r\u00e9si tokenre van sz\u00fcks\u00e9g." + }, + "step": { + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "device_class": "Eszk\u00f6zt\u00edpus", + "name": "N\u00e9v" + }, + "title": "A Vizio SmartCast Client be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "API-k\u00e9r\u00e9s id\u0151t\u00fall\u00e9p\u00e9se (m\u00e1sodpercben)", + "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga" + }, + "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat" + } + }, + "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/nl.json b/homeassistant/components/vizio/.translations/nl.json new file mode 100644 index 00000000000..bbc95d73bbc --- /dev/null +++ b/homeassistant/components/vizio/.translations/nl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_in_progress": "Configuratie stroom voor vizio component al in uitvoering.", + "already_setup": "Dit item is al ingesteld.", + "already_setup_with_diff_host_and_name": "Dit item lijkt al te zijn ingesteld met een andere host en naam op basis van het serienummer. Verwijder alle oude vermeldingen uit uw configuratie.yaml en uit het menu Integraties voordat u opnieuw probeert dit apparaat toe te voegen.", + "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", + "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.", + "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt.", + "updated_options": "Dit item is al ingesteld, maar de opties die in de configuratie zijn gedefinieerd komen niet overeen met de eerder ge\u00efmporteerde optiewaarden, dus de configuratie-invoer is dienovereenkomstig bijgewerkt.", + "updated_volume_step": "Dit item is al ingesteld, maar de volumestapgrootte in de configuratie komt niet overeen met het configuratie-item, dus het configuratie-item is dienovereenkomstig bijgewerkt." + }, + "error": { + "cant_connect": "Kan geen verbinding maken met het apparaat. [Bekijk de documenten] (https://www.home-assistant.io/integrations/vizio/) en controleer of:\n- Het apparaat is ingeschakeld\n- Het apparaat is aangesloten op het netwerk\n- De waarden die u ingevuld correct zijn\nvoordat u weer probeert om opnieuw in te dienen.", + "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", + "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.", + "tv_needs_token": "Wanneer het apparaattype `tv` is, dan is er een geldig toegangstoken nodig." + }, + "step": { + "user": { + "data": { + "access_token": "Toegangstoken", + "device_class": "Apparaattype", + "host": ":", + "name": "Naam" + }, + "title": "Vizio SmartCast Client instellen" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Time-out van API-aanvragen (seconden)", + "volume_step": "Volume Stapgrootte" + }, + "title": "Update Vizo SmartCast Opties" + } + }, + "title": "Update Vizo SmartCast Opties" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/hu.json b/homeassistant/components/withings/.translations/hu.json index 000e19c2067..503013e402f 100644 --- a/homeassistant/components/withings/.translations/hu.json +++ b/homeassistant/components/withings/.translations/hu.json @@ -2,12 +2,31 @@ "config": { "abort": { "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." + "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "no_flows": "Konfigur\u00e1lnia kell a Withings-et, miel\u0151tt hiteles\u00edtheti mag\u00e1t vele. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." + }, + "create_entry": { + "default": "A Withings sikeresen hiteles\u00edtett." }, "step": { "pick_implementation": { "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.", + "title": "Felhaszn\u00e1l\u00f3i profil." + }, + "user": { + "data": { + "profile": "Profil" + }, + "description": "V\u00e1lasszon egy felhaszn\u00e1l\u00f3i profilt, amelyet szeretn\u00e9, hogy a Home Assistant hozz\u00e1rendeljen a Withings profilhoz. \u00dcgyeljen arra, hogy ugyanazt a felhaszn\u00e1l\u00f3t v\u00e1lassza a Withings oldalon, k\u00fcl\u00f6nben az adatok nem lesznek megfelel\u0151en felcimk\u00e9zve.", + "title": "Felhaszn\u00e1l\u00f3i profil." } - } + }, + "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index c831561a439..0b01fc8c16a 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]" }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, "step": { + "pick_implementation": { + "title": "Kies Authenticatiemethode" + }, "profile": { "data": { "profile": "Profiel" diff --git a/homeassistant/components/wled/.translations/hu.json b/homeassistant/components/wled/.translations/hu.json new file mode 100644 index 00000000000..644b61ceb73 --- /dev/null +++ b/homeassistant/components/wled/.translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "connection_error": "Nem siker\u00fclt csatlakozni a WLED eszk\u00f6zh\u00f6z." + }, + "error": { + "connection_error": "Nem siker\u00fclt csatlakozni a WLED eszk\u00f6zh\u00f6z." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hosztn\u00e9v vagy IP c\u00edm" + }, + "description": "\u00c1ll\u00edtsa be a WLED-et, hogy integr\u00e1l\u00f3djon a Home Assistant alkalmaz\u00e1sba.", + "title": "Csatlakoztassa a WLED-t" + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 akarja adni a {name} `nev\u0171 WLED-et a Home Assistant-hez?", + "title": "Felfedezett WLED eszk\u00f6z" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 699d6ad658f5430f9e149c171dfa91da3a9f888e Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Sat, 8 Feb 2020 09:28:35 +0100 Subject: [PATCH 150/378] Add Minecraft Server Integration (#30992) * Add Minecraft Server integration * Add unit test for config flow * Fixed some review findings and increased unit test coverage * Fixed docstrings of new test cases * Removed unnecessary debug log messages * Added unique IDs and device infos and removed duplicate name validation * Attempt to fix unit test on CI * Return state OFF instead of UNAVAILABLE in case connection to server drops * Added property decorator to server properties, even less debug messages, improved sensor dispatcher connection and other review findings fixed * Moved special property handling to sensors, fixed name confusion in sensor entity, switch to HA const for scan_interval, simplified building players list string * Improved periodic update, speeded up unit tests * Added type hints, added callback decorator to entity update callback, added const.py to unit test exclusions * Changed state sensor to binary sensor, removed empty unit test file, added constants for icons and units * Let HA handle unknown state, check for None in description and players list sensor * Removed periods at end of log messages, removed constant for default host * Updated requirements_test_pre_commit.txt, fixed codespell findings * Use localhost as default host * Removed passing hass to entities, moved log message from init, moved host lower to vol, use proper patch library, patch library instead of own code * Replaced server properties with global instance attributes, removed config option scan_interval, switch back to async_track_time_interval * Removed description and players list sensors, added players list as state attributes to online players sensor, raise OSError instead of deprecated IOError, other minor review findings fixed * Use MAC address for unique_id in case of an IP address as host, added getmac to manifest.json, added invalid_ip to strings.json, added new test cases for changes in config_flow, replace all IOError's with OSError, other review findings fixed * Removed double assignment * Call get_mac_address async safe * Handle unavailable and unknown states to reach silver quality scale, added quality scale to manifest.json --- .coveragerc | 4 + CODEOWNERS | 1 + .../components/minecraft_server/__init__.py | 273 ++++++++++++++++++ .../minecraft_server/binary_sensor.py | 47 +++ .../minecraft_server/config_flow.py | 116 ++++++++ .../components/minecraft_server/const.py | 37 +++ .../components/minecraft_server/manifest.json | 10 + .../components/minecraft_server/sensor.py | 177 ++++++++++++ .../components/minecraft_server/strings.json | 24 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 4 + requirements_test_all.txt | 4 + tests/components/minecraft_server/__init__.py | 1 + .../minecraft_server/test_config_flow.py | 194 +++++++++++++ 14 files changed, 893 insertions(+) create mode 100644 homeassistant/components/minecraft_server/__init__.py create mode 100644 homeassistant/components/minecraft_server/binary_sensor.py create mode 100644 homeassistant/components/minecraft_server/config_flow.py create mode 100644 homeassistant/components/minecraft_server/const.py create mode 100644 homeassistant/components/minecraft_server/manifest.json create mode 100644 homeassistant/components/minecraft_server/sensor.py create mode 100644 homeassistant/components/minecraft_server/strings.json create mode 100644 tests/components/minecraft_server/__init__.py create mode 100644 tests/components/minecraft_server/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6e2ea5ba89b..369aaa3b4e0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -425,6 +425,10 @@ omit = homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py + homeassistant/components/minecraft_server/__init__.py + homeassistant/components/minecraft_server/binary_sensor.py + homeassistant/components/minecraft_server/const.py + homeassistant/components/minecraft_server/sensor.py homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 4078a45f990..c3f018ef83a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -212,6 +212,7 @@ homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff +homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py new file mode 100644 index 00000000000..789e4d8f1b8 --- /dev/null +++ b/homeassistant/components/minecraft_server/__init__.py @@ -0,0 +1,273 @@ +"""The Minecraft Server integration.""" + +import asyncio +from datetime import datetime, timedelta +import logging +from typing import Any, Dict + +from mcstatus.server import MinecraftServer as MCStatus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX + +PLATFORMS = ["binary_sensor", "sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up the Minecraft Server component.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: + """Set up Minecraft Server from a config entry.""" + domain_data = hass.data.setdefault(DOMAIN, {}) + + # Create and store server instance. + unique_id = config_entry.unique_id + _LOGGER.debug( + "Creating server instance for '%s' (host='%s', port=%s)", + config_entry.data[CONF_NAME], + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + ) + server = MinecraftServer(hass, unique_id, config_entry.data) + domain_data[unique_id] = server + await server.async_update() + server.start_periodic_update() + + # Set up platform(s). + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload Minecraft Server config entry.""" + unique_id = config_entry.unique_id + server = hass.data[DOMAIN][unique_id] + + # Unload platforms. + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + + # Clean up. + server.stop_periodic_update() + hass.data[DOMAIN].pop(unique_id) + + return True + + +class MinecraftServer: + """Representation of a Minecraft server.""" + + # Private constants + _MAX_RETRIES_PING = 3 + _MAX_RETRIES_STATUS = 3 + + def __init__( + self, hass: HomeAssistantType, unique_id: str, config_data: ConfigType + ) -> None: + """Initialize server instance.""" + self._hass = hass + + # Server data + self.unique_id = unique_id + self.name = config_data[CONF_NAME] + self.host = config_data[CONF_HOST] + self.port = config_data[CONF_PORT] + self.online = False + self._last_status_request_failed = False + + # 3rd party library instance + self._mc_status = MCStatus(self.host, self.port) + + # Data provided by 3rd party library + self.description = None + self.version = None + self.protocol_version = None + self.latency_time = None + self.players_online = None + self.players_max = None + self.players_list = None + + # Dispatcher signal name + self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" + + # Callback for stopping periodic update. + self._stop_periodic_update = None + + def start_periodic_update(self) -> None: + """Start periodic execution of update method.""" + self._stop_periodic_update = async_track_time_interval( + self._hass, self.async_update, timedelta(seconds=SCAN_INTERVAL) + ) + + def stop_periodic_update(self) -> None: + """Stop periodic execution of update method.""" + self._stop_periodic_update() + + async def async_check_connection(self) -> None: + """Check server connection using a 'ping' request and store result.""" + try: + await self._hass.async_add_executor_job( + self._mc_status.ping, self._MAX_RETRIES_PING + ) + self.online = True + except OSError as error: + _LOGGER.debug( + "Error occurred while trying to ping the server - OSError: %s", error + ) + self.online = False + + async def async_update(self, now: datetime = None) -> None: + """Get server data from 3rd party library and update properties.""" + # Check connection status. + server_online_old = self.online + await self.async_check_connection() + server_online = self.online + + # Inform user once about connection state changes if necessary. + if server_online_old and not server_online: + _LOGGER.warning("Connection to server lost") + elif not server_online_old and server_online: + _LOGGER.info("Connection to server (re-)established") + + # Update the server properties if server is online. + if server_online: + await self._async_status_request() + + # Notify sensors about new data. + async_dispatcher_send(self._hass, self.signal_name) + + async def _async_status_request(self) -> None: + """Request server status and update properties.""" + try: + status_response = await self._hass.async_add_executor_job( + self._mc_status.status, self._MAX_RETRIES_STATUS + ) + + # Got answer to request, update properties. + self.description = status_response.description["text"] + self.version = status_response.version.name + self.protocol_version = status_response.version.protocol + self.players_online = status_response.players.online + self.players_max = status_response.players.max + self.latency_time = status_response.latency + self.players_list = [] + if status_response.players.sample is not None: + for player in status_response.players.sample: + self.players_list.append(player.name) + + # Inform user once about successful update if necessary. + if self._last_status_request_failed: + _LOGGER.info("Updating the server properties succeeded again") + self._last_status_request_failed = False + except OSError as error: + # No answer to request, set all properties to unknown. + self.description = None + self.version = None + self.protocol_version = None + self.players_online = None + self.players_max = None + self.latency_time = None + self.players_list = None + + # Inform user once about failed update if necessary. + if not self._last_status_request_failed: + _LOGGER.warning( + "Updating the server properties failed - OSError: %s", error, + ) + self._last_status_request_failed = True + + +class MinecraftServerEntity(Entity): + """Representation of a Minecraft Server base entity.""" + + def __init__( + self, server: MinecraftServer, type_name: str, icon: str, device_class: str + ) -> None: + """Initialize base entity.""" + self._server = server + self._name = f"{server.name} {type_name}" + self._icon = icon + self._unique_id = f"{self._server.unique_id}-{type_name}" + self._device_info = { + "identifiers": {(DOMAIN, self._server.unique_id)}, + "name": self._server.name, + "manufacturer": MANUFACTURER, + "model": f"Minecraft Server ({self._server.version})", + "sw_version": self._server.protocol_version, + } + self._device_class = device_class + self._device_state_attributes = None + self._disconnect_dispatcher = None + + @property + def name(self) -> str: + """Return name.""" + return self._name + + @property + def unique_id(self) -> str: + """Return unique ID.""" + return self._unique_id + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information.""" + return self._device_info + + @property + def device_class(self) -> str: + """Return device class.""" + return self._device_class + + @property + def icon(self) -> str: + """Return icon.""" + return self._icon + + @property + def should_poll(self) -> bool: + """Disable polling.""" + return False + + async def async_update(self) -> None: + """Fetch data from the server.""" + raise NotImplementedError() + + async def async_added_to_hass(self) -> None: + """Connect dispatcher to signal from server.""" + self._disconnect_dispatcher = async_dispatcher_connect( + self.hass, self._server.signal_name, self._update_callback + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher before removal.""" + self._disconnect_dispatcher() + + @callback + def _update_callback(self) -> None: + """Triggers update of properties after receiving signal from server.""" + self.async_schedule_update_ha_state(force_refresh=True) diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py new file mode 100644 index 00000000000..cde2a414900 --- /dev/null +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -0,0 +1,47 @@ +"""The Minecraft Server binary sensor platform.""" + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import MinecraftServer, MinecraftServerEntity +from .const import DOMAIN, ICON_STATUS, NAME_STATUS + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Minecraft Server binary sensor platform.""" + server = hass.data[DOMAIN][config_entry.unique_id] + + # Create entities list. + entities = [MinecraftServerStatusBinarySensor(server)] + + # Add binary sensor entities. + async_add_entities(entities, True) + + +class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorDevice): + """Representation of a Minecraft Server status binary sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize status binary sensor.""" + super().__init__( + server=server, + type_name=NAME_STATUS, + icon=ICON_STATUS, + device_class=DEVICE_CLASS_CONNECTIVITY, + ) + self._is_on = False + + @property + def is_on(self) -> bool: + """Return binary state.""" + return self._is_on + + async def async_update(self) -> None: + """Update status.""" + self._is_on = self._server.online diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py new file mode 100644 index 00000000000..8c6049a2c1b --- /dev/null +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for Minecraft Server integration.""" +from functools import partial +import ipaddress + +import getmac +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT + +from . import MinecraftServer +from .const import ( # pylint: disable=unused-import + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) + + +class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Minecraft Server.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + # User inputs. + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + unique_id = "" + + # Check if 'host' is a valid IP address and if so, get the MAC address. + ip_address = None + mac_address = None + try: + ip_address = ipaddress.ip_address(host) + except ValueError: + # Host is not a valid IP address. + pass + else: + # Host is a valid IP address. + if ip_address.version == 4: + # Address type is IPv4. + params = {"ip": host} + else: + # Address type is IPv6. + params = {"ip6": host} + mac_address = await self.hass.async_add_executor_job( + partial(getmac.get_mac_address, **params) + ) + + # Validate IP address via valid MAC address. + if ip_address is not None and mac_address is None: + errors["base"] = "invalid_ip" + # Validate port configuration (limit to user and dynamic port range). + elif (port < 1024) or (port > 65535): + errors["base"] = "invalid_port" + # Validate host and port via ping request to server. + else: + # Build unique_id. + if ip_address is not None: + # Since IP addresses can change and therefore are not allowed in a + # unique_id, fall back to the MAC address. + unique_id = f"{mac_address}-{port}" + else: + # Use host name in unique_id (host names should not change). + unique_id = f"{host}-{port}" + + # Abort in case the host was already configured before. + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + # Create server instance with configuration data and try pinging the server. + server = MinecraftServer(self.hass, unique_id, user_input) + await server.async_check_connection() + if not server.online: + # Host or port invalid or server not reachable. + errors["base"] = "cannot_connect" + else: + # Configuration data are available and no error was detected, create configuration entry. + return self.async_create_entry( + title=f"{host}:{port}", data=user_input + ) + + # Show configuration form (default form in case of no user_input, + # form filled with user_input and eventually with errors otherwise). + return self._show_config_form(user_input, errors) + + def _show_config_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) + ): vol.All(str, vol.Lower), + vol.Optional( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py new file mode 100644 index 00000000000..c3ab6615481 --- /dev/null +++ b/homeassistant/components/minecraft_server/const.py @@ -0,0 +1,37 @@ +"""Constants for the Minecraft Server integration.""" + +ATTR_PLAYERS_LIST = "players_list" + +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Minecraft Server" +DEFAULT_PORT = 25565 + +DOMAIN = "minecraft_server" + +ICON_LATENCY_TIME = "mdi:signal" +ICON_PLAYERS_MAX = "mdi:account-multiple" +ICON_PLAYERS_ONLINE = "mdi:account-multiple" +ICON_PROTOCOL_VERSION = "mdi:numeric" +ICON_STATUS = "mdi:lan" +ICON_VERSION = "mdi:numeric" + +KEY_SERVERS = "servers" + +MANUFACTURER = "Mojang AB" + +NAME_LATENCY_TIME = "Latency Time" +NAME_PLAYERS_MAX = "Players Max" +NAME_PLAYERS_ONLINE = "Players Online" +NAME_PROTOCOL_VERSION = "Protocol Version" +NAME_STATUS = "Status" +NAME_VERSION = "Version" + +SCAN_INTERVAL = 60 + +SIGNAL_NAME_PREFIX = f"signal_{DOMAIN}" + +UNIT_LATENCY_TIME = "ms" +UNIT_PLAYERS_MAX = "players" +UNIT_PLAYERS_ONLINE = "players" +UNIT_PROTOCOL_VERSION = None +UNIT_VERSION = None diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json new file mode 100644 index 00000000000..1dda76dee77 --- /dev/null +++ b/homeassistant/components/minecraft_server/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "minecraft_server", + "name": "Minecraft Server", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/minecraft_server", + "requirements": ["getmac==0.8.1", "mcstatus==2.3.0"], + "dependencies": [], + "codeowners": ["@elmurato"], + "quality_scale": "silver" +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py new file mode 100644 index 00000000000..0b37a7d979b --- /dev/null +++ b/homeassistant/components/minecraft_server/sensor.py @@ -0,0 +1,177 @@ +"""The Minecraft Server sensor platform.""" + +import logging +from typing import Any, Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import MinecraftServer, MinecraftServerEntity +from .const import ( + ATTR_PLAYERS_LIST, + DOMAIN, + ICON_LATENCY_TIME, + ICON_PLAYERS_MAX, + ICON_PLAYERS_ONLINE, + ICON_PROTOCOL_VERSION, + ICON_VERSION, + NAME_LATENCY_TIME, + NAME_PLAYERS_MAX, + NAME_PLAYERS_ONLINE, + NAME_PROTOCOL_VERSION, + NAME_VERSION, + UNIT_LATENCY_TIME, + UNIT_PLAYERS_MAX, + UNIT_PLAYERS_ONLINE, + UNIT_PROTOCOL_VERSION, + UNIT_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Minecraft Server sensor platform.""" + server = hass.data[DOMAIN][config_entry.unique_id] + + # Create entities list. + entities = [ + MinecraftServerVersionSensor(server), + MinecraftServerProtocolVersionSensor(server), + MinecraftServerLatencyTimeSensor(server), + MinecraftServerPlayersOnlineSensor(server), + MinecraftServerPlayersMaxSensor(server), + ] + + # Add sensor entities. + async_add_entities(entities, True) + + +class MinecraftServerSensorEntity(MinecraftServerEntity): + """Representation of a Minecraft Server sensor base entity.""" + + def __init__( + self, + server: MinecraftServer, + type_name: str, + icon: str = None, + unit: str = None, + device_class: str = None, + ) -> None: + """Initialize sensor base entity.""" + super().__init__(server, type_name, icon, device_class) + self._state = None + self._unit = unit + + @property + def available(self) -> bool: + """Return sensor availability.""" + return self._server.online + + @property + def state(self) -> Any: + """Return sensor state.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return sensor measurement unit.""" + return self._unit + + +class MinecraftServerVersionSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server version sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize version sensor.""" + super().__init__( + server=server, type_name=NAME_VERSION, icon=ICON_VERSION, unit=UNIT_VERSION + ) + + async def async_update(self) -> None: + """Update version.""" + self._state = self._server.version + + +class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server protocol version sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize protocol version sensor.""" + super().__init__( + server=server, + type_name=NAME_PROTOCOL_VERSION, + icon=ICON_PROTOCOL_VERSION, + unit=UNIT_PROTOCOL_VERSION, + ) + + async def async_update(self) -> None: + """Update protocol version.""" + self._state = self._server.protocol_version + + +class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server latency time sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize latency time sensor.""" + super().__init__( + server=server, + type_name=NAME_LATENCY_TIME, + icon=ICON_LATENCY_TIME, + unit=UNIT_LATENCY_TIME, + ) + + async def async_update(self) -> None: + """Update latency time.""" + self._state = self._server.latency_time + + +class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server online players sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize online players sensor.""" + super().__init__( + server=server, + type_name=NAME_PLAYERS_ONLINE, + icon=ICON_PLAYERS_ONLINE, + unit=UNIT_PLAYERS_ONLINE, + ) + + async def async_update(self) -> None: + """Update online players state and device state attributes.""" + self._state = self._server.players_online + + device_state_attributes = None + players_list = self._server.players_list + + if players_list is not None: + if len(players_list) != 0: + device_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} + + self._device_state_attributes = device_state_attributes + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return players list in device state attributes.""" + return self._device_state_attributes + + +class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server maximum number of players sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize maximum number of players sensor.""" + super().__init__( + server=server, + type_name=NAME_PLAYERS_MAX, + icon=ICON_PLAYERS_MAX, + unit=UNIT_PLAYERS_MAX, + ) + + async def async_update(self) -> None: + """Update maximum number of players.""" + self._state = self._server.players_max diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json new file mode 100644 index 00000000000..7743d940be6 --- /dev/null +++ b/homeassistant/components/minecraft_server/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Minecraft Server", + "step": { + "user": { + "title": "Link your Minecraft Server", + "description": "Set up your Minecraft Server instance to allow monitoring.", + "data": { + "name": "Name", + "host": "Host", + "port": "Port" + } + } + }, + "error": { + "invalid_port": "Port must be in range from 1024 to 65535. Please correct it and try again.", + "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.", + "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again." + }, + "abort": { + "already_configured": "Host is already configured." + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 83f7d4cfcfa..4872b08e9fc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -57,6 +57,7 @@ FLOWS = [ "met", "meteo_france", "mikrotik", + "minecraft_server", "mobile_app", "mqtt", "neato", diff --git a/requirements_all.txt b/requirements_all.txt index d83f0884def..a0c76d3ab22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -585,6 +585,7 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.braviatv # homeassistant.components.huawei_lte # homeassistant.components.kef +# homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker getmac==0.8.1 @@ -837,6 +838,9 @@ maxcube-api==0.1.0 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 +# homeassistant.components.minecraft_server +mcstatus==2.3.0 + # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8d873b2fd6..33942e1b244 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,6 +210,7 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.braviatv # homeassistant.components.huawei_lte # homeassistant.components.kef +# homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker getmac==0.8.1 @@ -301,6 +302,9 @@ luftdaten==0.6.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 +# homeassistant.components.minecraft_server +mcstatus==2.3.0 + # homeassistant.components.meteo_france meteofrance==0.3.7 diff --git a/tests/components/minecraft_server/__init__.py b/tests/components/minecraft_server/__init__.py new file mode 100644 index 00000000000..36a1bb3f69d --- /dev/null +++ b/tests/components/minecraft_server/__init__.py @@ -0,0 +1 @@ +"""Tests for the Minecraft Server integration.""" diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py new file mode 100644 index 00000000000..30626fbdcb0 --- /dev/null +++ b/tests/components/minecraft_server/test_config_flow.py @@ -0,0 +1,194 @@ +"""Test the Minecraft Server config flow.""" + +from asynctest import patch +from mcstatus.pinger import PingResponse + +from homeassistant.components.minecraft_server.const import ( + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +STATUS_RESPONSE_RAW = { + "description": {"text": "Dummy Description"}, + "version": {"name": "Dummy Version", "protocol": 123}, + "players": { + "online": 3, + "max": 10, + "sample": [ + {"name": "Player 1", "id": "1"}, + {"name": "Player 2", "id": "2"}, + {"name": "Player 3", "id": "3"}, + ], + }, +} + +USER_INPUT = { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: "mc.dummyserver.com", + CONF_PORT: DEFAULT_PORT, +} + +USER_INPUT_IPV4 = { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, +} + +USER_INPUT_IPV6 = { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: "::ffff:0101:0101", + CONF_PORT: DEFAULT_PORT, +} + +USER_INPUT_PORT_TOO_SMALL = { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: "mc.dummyserver.com", + CONF_PORT: 1023, +} + +USER_INPUT_PORT_TOO_LARGE = { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: "mc.dummyserver.com", + CONF_PORT: 65536, +} + + +async def test_show_config_form(hass: HomeAssistantType) -> None: + """Test if initial configuration form is shown.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_invalid_ip(hass: HomeAssistantType) -> None: + """Test error in case of an invalid IP address.""" + with patch("getmac.get_mac_address", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_ip"} + + +async def test_same_host(hass: HomeAssistantType) -> None: + """Test abort in case of same host name.""" + unique_id = f"{USER_INPUT[CONF_HOST]}-{USER_INPUT[CONF_PORT]}" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, unique_id=unique_id, data=USER_INPUT + ) + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_port_too_small(hass: HomeAssistantType) -> None: + """Test error in case of a too small port.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_port"} + + +async def test_port_too_large(hass: HomeAssistantType) -> None: + """Test error in case of a too large port.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_port"} + + +async def test_connection_failed(hass: HomeAssistantType) -> None: + """Test error in case of a failed connection.""" + with patch("mcstatus.server.MinecraftServer.ping", side_effect=OSError): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None: + """Test config entry in case of a successful connection with a host name.""" + with patch("mcstatus.server.MinecraftServer.ping", return_value=50): + with patch( + "mcstatus.server.MinecraftServer.status", + return_value=PingResponse(STATUS_RESPONSE_RAW), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{USER_INPUT[CONF_HOST]}:{USER_INPUT[CONF_PORT]}" + assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] + assert result["data"][CONF_HOST] == USER_INPUT[CONF_HOST] + assert result["data"][CONF_PORT] == USER_INPUT[CONF_PORT] + + +async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None: + """Test config entry in case of a successful connection with an IPv4 address.""" + with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"): + with patch("mcstatus.server.MinecraftServer.ping", return_value=50): + with patch( + "mcstatus.server.MinecraftServer.status", + return_value=PingResponse(STATUS_RESPONSE_RAW), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["title"] + == f"{USER_INPUT_IPV4[CONF_HOST]}:{USER_INPUT_IPV4[CONF_PORT]}" + ) + assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME] + assert result["data"][CONF_HOST] == USER_INPUT_IPV4[CONF_HOST] + assert result["data"][CONF_PORT] == USER_INPUT_IPV4[CONF_PORT] + + +async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None: + """Test config entry in case of a successful connection with an IPv6 address.""" + with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"): + with patch("mcstatus.server.MinecraftServer.ping", return_value=50): + with patch( + "mcstatus.server.MinecraftServer.status", + return_value=PingResponse(STATUS_RESPONSE_RAW), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6 + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["title"] + == f"{USER_INPUT_IPV6[CONF_HOST]}:{USER_INPUT_IPV6[CONF_PORT]}" + ) + assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME] + assert result["data"][CONF_HOST] == USER_INPUT_IPV6[CONF_HOST] + assert result["data"][CONF_PORT] == USER_INPUT_IPV6[CONF_PORT] From baa9184b33802de9197a98e110664305263f8bbc Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 8 Feb 2020 10:07:20 +0100 Subject: [PATCH 151/378] Extract services from init.py for HomematicIP Cloud (#31376) * Extract services from init.py for HomematicIP Cloud * add ServiceCallType --- .../components/homematicip_cloud/__init__.py | 271 +------------ .../components/homematicip_cloud/services.py | 365 ++++++++++++++++++ 2 files changed, 372 insertions(+), 264 deletions(-) create mode 100644 homeassistant/components/homematicip_cloud/services.py diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index efc7245de8b..d1982e289a3 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,20 +1,13 @@ """Support for HomematicIP Cloud devices.""" import logging -from pathlib import Path -from typing import Optional -from homematicip.aio.device import AsyncSwitchMeasuring -from homematicip.aio.group import AsyncHeatingGroup -from homematicip.aio.home import AsyncHome -from homematicip.base.helpers import handle_config import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( @@ -27,29 +20,10 @@ from .const import ( ) from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 +from .services import async_setup_services, async_unload_services _LOGGER = logging.getLogger(__name__) -ATTR_ACCESSPOINT_ID = "accesspoint_id" -ATTR_ANONYMIZE = "anonymize" -ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" -ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" -ATTR_CONFIG_OUTPUT_PATH = "config_output_path" -ATTR_DURATION = "duration" -ATTR_ENDTIME = "endtime" -ATTR_TEMPERATURE = "temperature" - -DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" - -SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" -SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" -SERVICE_ACTIVATE_VACATION = "activate_vacation" -SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" -SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" -SERVICE_DUMP_HAP_CONFIG = "dump_hap_config" -SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter" -SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" - CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=[]): vol.All( @@ -68,59 +42,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema( - { - vol.Required(ATTR_DURATION): cv.positive_int, - vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), - } -) - -SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema( - { - vol.Required(ATTR_ENDTIME): cv.datetime, - vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), - } -) - -SCHEMA_ACTIVATE_VACATION = vol.Schema( - { - vol.Required(ATTR_ENDTIME): cv.datetime, - vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All( - vol.Coerce(float), vol.Range(min=0, max=55) - ), - vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), - } -) - -SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema( - {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} -) - -SCHEMA_DEACTIVATE_VACATION = vol.Schema( - {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} -) - -SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): comp_entity_ids, - vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int, - } -) - -SCHEMA_DUMP_HAP_CONFIG = vol.Schema( - { - vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string, - vol.Optional( - ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX - ): cv.string, - vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean, - } -) - -SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( - {vol.Required(ATTR_ENTITY_ID): comp_entity_ids} -) - async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -145,189 +66,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) ) - async def _async_activate_eco_mode_with_duration(service) -> None: - """Service to activate eco mode with duration.""" - duration = service.data[ATTR_DURATION] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.activate_absence_with_duration(duration) - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_absence_with_duration(duration) - - hass.services.async_register( - DOMAIN, - SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, - _async_activate_eco_mode_with_duration, - schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, - ) - - async def _async_activate_eco_mode_with_period(service) -> None: - """Service to activate eco mode with period.""" - endtime = service.data[ATTR_ENDTIME] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.activate_absence_with_period(endtime) - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_absence_with_period(endtime) - - hass.services.async_register( - DOMAIN, - SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, - _async_activate_eco_mode_with_period, - schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, - ) - - async def _async_activate_vacation(service) -> None: - """Service to activate vacation.""" - endtime = service.data[ATTR_ENDTIME] - temperature = service.data[ATTR_TEMPERATURE] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.activate_vacation(endtime, temperature) - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_vacation(endtime, temperature) - - hass.services.async_register( - DOMAIN, - SERVICE_ACTIVATE_VACATION, - _async_activate_vacation, - schema=SCHEMA_ACTIVATE_VACATION, - ) - - async def _async_deactivate_eco_mode(service) -> None: - """Service to deactivate eco mode.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.deactivate_absence() - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.deactivate_absence() - - hass.services.async_register( - DOMAIN, - SERVICE_DEACTIVATE_ECO_MODE, - _async_deactivate_eco_mode, - schema=SCHEMA_DEACTIVATE_ECO_MODE, - ) - - async def _async_deactivate_vacation(service) -> None: - """Service to deactivate vacation.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.deactivate_vacation() - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.deactivate_vacation() - - hass.services.async_register( - DOMAIN, - SERVICE_DEACTIVATE_VACATION, - _async_deactivate_vacation, - schema=SCHEMA_DEACTIVATE_VACATION, - ) - - async def _set_active_climate_profile(service) -> None: - """Service to set the active climate profile.""" - entity_id_list = service.data[ATTR_ENTITY_ID] - climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 - - for hap in hass.data[DOMAIN].values(): - if entity_id_list != "all": - for entity_id in entity_id_list: - group = hap.hmip_device_by_entity_id.get(entity_id) - if group and isinstance(group, AsyncHeatingGroup): - await group.set_active_profile(climate_profile_index) - else: - for group in hap.home.groups: - if isinstance(group, AsyncHeatingGroup): - await group.set_active_profile(climate_profile_index) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_ACTIVE_CLIMATE_PROFILE, - _set_active_climate_profile, - schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, - ) - - async def _async_dump_hap_config(service) -> None: - """Service to dump the configuration of a Homematic IP Access Point.""" - config_path = ( - service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir - ) - config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] - anonymize = service.data[ATTR_ANONYMIZE] - - for hap in hass.data[DOMAIN].values(): - hap_sgtin = hap.config_entry.unique_id - - if anonymize: - hap_sgtin = hap_sgtin[-4:] - - file_name = f"{config_file_prefix}_{hap_sgtin}.json" - path = Path(config_path) - config_file = path / file_name - - json_state = await hap.home.download_configuration() - json_state = handle_config(json_state, anonymize) - - config_file.write_text(json_state, encoding="utf8") - - hass.services.async_register( - DOMAIN, - SERVICE_DUMP_HAP_CONFIG, - _async_dump_hap_config, - schema=SCHEMA_DUMP_HAP_CONFIG, - ) - - async def _async_reset_energy_counter(service): - """Service to reset the energy counter.""" - entity_id_list = service.data[ATTR_ENTITY_ID] - - for hap in hass.data[DOMAIN].values(): - if entity_id_list != "all": - for entity_id in entity_id_list: - device = hap.hmip_device_by_entity_id.get(entity_id) - if device and isinstance(device, AsyncSwitchMeasuring): - await device.reset_energy_counter() - else: - for device in hap.home.devices: - if isinstance(device, AsyncSwitchMeasuring): - await device.reset_energy_counter() - - hass.helpers.service.async_register_admin_service( - DOMAIN, - SERVICE_RESET_ENERGY_COUNTER, - _async_reset_energy_counter, - schema=SCHEMA_RESET_ENERGY_COUNTER, - ) - - def _get_home(hapid: str) -> Optional[AsyncHome]: - """Return a HmIP home.""" - hap = hass.data[DOMAIN].get(hapid) - if hap: - return hap.home - - _LOGGER.info("No matching access point found for access point id %s", hapid) - return None - return True @@ -348,6 +86,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if not await hap.async_setup(): return False + await async_setup_services(hass) + # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection hap.reset_connection_listener = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, hap.shutdown @@ -373,4 +113,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.unique_id) hap.reset_connection_listener() + + await async_unload_services(hass) + return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py new file mode 100644 index 00000000000..19d50893b9b --- /dev/null +++ b/homeassistant/components/homematicip_cloud/services.py @@ -0,0 +1,365 @@ +"""Support for HomematicIP Cloud devices.""" +import logging +from pathlib import Path +from typing import Optional + +from homematicip.aio.device import AsyncSwitchMeasuring +from homematicip.aio.group import AsyncHeatingGroup +from homematicip.aio.home import AsyncHome +from homematicip.base.helpers import handle_config +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import comp_entity_ids +from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) +HOMEMATICIP_CLOUD_SERVICES = "homematicip_cloud_services" + +ATTR_ACCESSPOINT_ID = "accesspoint_id" +ATTR_ANONYMIZE = "anonymize" +ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" +ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" +ATTR_CONFIG_OUTPUT_PATH = "config_output_path" +ATTR_DURATION = "duration" +ATTR_ENDTIME = "endtime" +ATTR_TEMPERATURE = "temperature" + +DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" + +SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" +SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" +SERVICE_ACTIVATE_VACATION = "activate_vacation" +SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" +SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_DUMP_HAP_CONFIG = "dump_hap_config" +SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter" +SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" + +HMIPC_SERVICES2 = { + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION: "_async_activate_eco_mode_with_duration", + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD: "_async_activate_eco_mode_with_period", + SERVICE_ACTIVATE_VACATION: "_async_activate_vacation", + SERVICE_DEACTIVATE_ECO_MODE: "SERVICE_DEACTIVATE_ECO_MODE", + SERVICE_DEACTIVATE_VACATION: "_async_deactivate_vacation", + SERVICE_DUMP_HAP_CONFIG: "_async_dump_hap_config", + SERVICE_RESET_ENERGY_COUNTER: "_async_reset_energy_counter", + SERVICE_SET_ACTIVE_CLIMATE_PROFILE: "_set_active_climate_profile", +} + +HMIPC_SERVICES = [ + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, + SERVICE_ACTIVATE_VACATION, + SERVICE_DEACTIVATE_ECO_MODE, + SERVICE_DEACTIVATE_VACATION, + SERVICE_DUMP_HAP_CONFIG, + SERVICE_RESET_ENERGY_COUNTER, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, +] + +SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema( + { + vol.Required(ATTR_DURATION): cv.positive_int, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + +SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema( + { + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + +SCHEMA_ACTIVATE_VACATION = vol.Schema( + { + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All( + vol.Coerce(float), vol.Range(min=0, max=55) + ), + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + +SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema( + {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} +) + +SCHEMA_DEACTIVATE_VACATION = vol.Schema( + {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} +) + +SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): comp_entity_ids, + vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int, + } +) + +SCHEMA_DUMP_HAP_CONFIG = vol.Schema( + { + vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string, + vol.Optional( + ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX + ): cv.string, + vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean, + } +) + +SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): comp_entity_ids} +) + + +async def async_setup_services(hass: HomeAssistantType) -> None: + """Set up the HomematicIP Cloud services.""" + + if hass.data.get(HOMEMATICIP_CLOUD_SERVICES, False): + return + + hass.data[HOMEMATICIP_CLOUD_SERVICES] = True + + async def async_call_hmipc_service(service: ServiceCallType): + """Call correct HomematicIP Cloud service.""" + service_name = service.service + + if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION: + await _async_activate_eco_mode_with_duration(hass, service) + elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD: + await _async_activate_eco_mode_with_period(hass, service) + elif service_name == SERVICE_ACTIVATE_VACATION: + await _async_activate_vacation(hass, service) + elif service_name == SERVICE_DEACTIVATE_ECO_MODE: + await _async_deactivate_eco_mode(hass, service) + elif service_name == SERVICE_DEACTIVATE_VACATION: + await _async_deactivate_vacation(hass, service) + elif service_name == SERVICE_DUMP_HAP_CONFIG: + await _async_dump_hap_config(hass, service) + elif service_name == SERVICE_RESET_ENERGY_COUNTER: + await _async_reset_energy_counter(hass, service) + elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE: + await _set_active_climate_profile(hass, service) + + hass.services.async_register( + DOMAIN, + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, + async_call_hmipc_service, + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, + async_call_hmipc_service, + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ACTIVATE_VACATION, + async_call_hmipc_service, + schema=SCHEMA_ACTIVATE_VACATION, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DEACTIVATE_ECO_MODE, + async_call_hmipc_service, + schema=SCHEMA_DEACTIVATE_ECO_MODE, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DEACTIVATE_VACATION, + async_call_hmipc_service, + schema=SCHEMA_DEACTIVATE_VACATION, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + async_call_hmipc_service, + schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DUMP_HAP_CONFIG, + async_call_hmipc_service, + schema=SCHEMA_DUMP_HAP_CONFIG, + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_RESET_ENERGY_COUNTER, + async_call_hmipc_service, + schema=SCHEMA_RESET_ENERGY_COUNTER, + ) + + +async def async_unload_services(hass): + """Unload HomematicIP Cloud services.""" + if hass.data[DOMAIN]: + return + + if not hass.data.get(HOMEMATICIP_CLOUD_SERVICES): + return + + hass.data[HOMEMATICIP_CLOUD_SERVICES] = False + + for hmipc_service in HMIPC_SERVICES: + hass.services.async_remove(DOMAIN, hmipc_service) + + +async def _async_activate_eco_mode_with_duration( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to activate eco mode with duration.""" + duration = service.data[ATTR_DURATION] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.activate_absence_with_duration(duration) + else: + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_duration(duration) + + +async def _async_activate_eco_mode_with_period( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to activate eco mode with period.""" + endtime = service.data[ATTR_ENDTIME] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.activate_absence_with_period(endtime) + else: + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_period(endtime) + + +async def _async_activate_vacation( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to activate vacation.""" + endtime = service.data[ATTR_ENDTIME] + temperature = service.data[ATTR_TEMPERATURE] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.activate_vacation(endtime, temperature) + else: + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_vacation(endtime, temperature) + + +async def _async_deactivate_eco_mode( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to deactivate eco mode.""" + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.deactivate_absence() + else: + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_absence() + + +async def _async_deactivate_vacation( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to deactivate vacation.""" + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.deactivate_vacation() + else: + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_vacation() + + +async def _set_active_climate_profile( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to set the active climate profile.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + group = hap.hmip_device_by_entity_id.get(entity_id) + if group and isinstance(group, AsyncHeatingGroup): + await group.set_active_profile(climate_profile_index) + else: + for group in hap.home.groups: + if isinstance(group, AsyncHeatingGroup): + await group.set_active_profile(climate_profile_index) + + +async def _async_dump_hap_config( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to dump the configuration of a Homematic IP Access Point.""" + config_path = service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir + config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] + anonymize = service.data[ATTR_ANONYMIZE] + + for hap in hass.data[DOMAIN].values(): + hap_sgtin = hap.config_entry.unique_id + + if anonymize: + hap_sgtin = hap_sgtin[-4:] + + file_name = f"{config_file_prefix}_{hap_sgtin}.json" + path = Path(config_path) + config_file = path / file_name + + json_state = await hap.home.download_configuration() + json_state = handle_config(json_state, anonymize) + + config_file.write_text(json_state, encoding="utf8") + + +async def _async_reset_energy_counter( + hass: HomeAssistantType, service: ServiceCallType +): + """Service to reset the energy counter.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + device = hap.hmip_device_by_entity_id.get(entity_id) + if device and isinstance(device, AsyncSwitchMeasuring): + await device.reset_energy_counter() + else: + for device in hap.home.devices: + if isinstance(device, AsyncSwitchMeasuring): + await device.reset_energy_counter() + + +def _get_home(hass: HomeAssistantType, hapid: str) -> Optional[AsyncHome]: + """Return a HmIP home.""" + hap = hass.data[DOMAIN].get(hapid) + if hap: + return hap.home + + _LOGGER.info("No matching access point found for access point id %s", hapid) + return None From 0823ee43852d4a93bf764ba1c6621bab7eb43f3c Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 8 Feb 2020 12:03:35 +0100 Subject: [PATCH 152/378] Fix exceptions when using newer Samsung TVs (#31602) * try to fix websocket problems * use tuple * catch websocket exceptions * typo --- .../components/samsungtv/config_flow.py | 19 ++++++++------ homeassistant/components/samsungtv/const.py | 2 -- .../components/samsungtv/test_config_flow.py | 25 ++++++++++++++++++- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index debe7349b6c..e52123297ab 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -5,6 +5,7 @@ from urllib.parse import urlparse from samsungctl import Remote from samsungctl.exceptions import AccessDenied, UnhandledResponse import voluptuous as vol +from websocket import WebSocketException from homeassistant import config_entries from homeassistant.components.ssdp import ( @@ -23,7 +24,7 @@ from homeassistant.const import ( ) # pylint:disable=unused-import -from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER, METHODS +from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) @@ -32,6 +33,12 @@ RESULT_SUCCESS = "success" RESULT_NOT_SUCCESSFUL = "not_successful" RESULT_NOT_SUPPORTED = "not_supported" +SUPPORTED_METHODS = ( + {"method": "websocket", "timeout": 1}, + # We need this high timeout because waiting for auth popup is just an open socket + {"method": "legacy", "timeout": 31}, +) + def _get_ip(host): if host is None: @@ -76,27 +83,25 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _try_connect(self): """Try to connect and check auth.""" - for method in METHODS: + for cfg in SUPPORTED_METHODS: config = { "name": "HomeAssistant", "description": "HomeAssistant", "id": "ha.component.samsung", "host": self._host, - "method": method, "port": self._port, - # We need this high timeout because waiting for auth popup is just an open socket - "timeout": 31, } + config.update(cfg) try: LOGGER.debug("Try config: %s", config) with Remote(config.copy()): LOGGER.debug("Working config: %s", config) - self._method = method + self._method = cfg["method"] return RESULT_SUCCESS except AccessDenied: LOGGER.debug("Working but denied config: %s", config) return RESULT_AUTH_MISSING - except UnhandledResponse: + except (UnhandledResponse, WebSocketException): LOGGER.debug("Working but unsupported config: %s", config) return RESULT_NOT_SUPPORTED except OSError as err: diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index ea893390a5b..46f6fb59a8c 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -9,5 +9,3 @@ DEFAULT_NAME = "Samsung TV" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" CONF_ON_ACTION = "turn_on_action" - -METHODS = ("websocket", "legacy") diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 9c8ec3a9a09..91ee8a7205f 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import call, patch from asynctest import mock import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse +from websocket import WebSocketProtocolException from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, @@ -42,7 +43,7 @@ AUTODETECT_WEBSOCKET = { "method": "websocket", "port": None, "host": "fake_host", - "timeout": 31, + "timeout": 1, } AUTODETECT_LEGACY = { "name": "HomeAssistant", @@ -245,6 +246,28 @@ async def test_ssdp_not_supported(hass): assert result["reason"] == "not_supported" +async def test_ssdp_not_supported_2(hass): + """Test starting a flow from discovery for not supported device.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=WebSocketProtocolException("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # device not supported + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + async def test_ssdp_not_successful(hass): """Test starting a flow from discovery but no device found.""" with patch( From 57ab30d5345bc569a13195f9412a9ad2de327195 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 8 Feb 2020 06:50:50 -0500 Subject: [PATCH 153/378] Bump ZHA dependencies. (#31619) Bump up zigpy-homeassistant==0.13.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 c56544d1784..f5ec96690bc 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows-homeassistant==0.13.2", "zha-quirks==0.0.32", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.13.1", + "zigpy-homeassistant==0.13.2", "zigpy-xbee-homeassistant==0.9.0", "zigpy-zigate==0.5.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index a0c76d3ab22..5c5885c8e80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2131,7 +2131,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.13.1 +zigpy-homeassistant==0.13.2 # homeassistant.components.zha zigpy-xbee-homeassistant==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33942e1b244..e54982bf74f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -718,7 +718,7 @@ zha-quirks==0.0.32 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.13.1 +zigpy-homeassistant==0.13.2 # homeassistant.components.zha zigpy-xbee-homeassistant==0.9.0 From 111050bea936646325caa3a2a7b84860597dd41c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Feb 2020 04:10:59 -0800 Subject: [PATCH 154/378] Clean up core services (#31509) * Clean up core services * Fix conversation test --- .../components/conversation/__init__.py | 19 ++- homeassistant/components/group/__init__.py | 7 +- .../components/homeassistant/__init__.py | 53 ++++--- homeassistant/components/intent/__init__.py | 19 ++- tests/components/conversation/test_init.py | 8 +- tests/components/homeassistant/test_init.py | 143 +++++++----------- tests/components/intent/test_init.py | 94 ++++++++++++ 7 files changed, 222 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 158a365981b..91031c141dd 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -131,9 +131,22 @@ class ConversationProcessView(http.HomeAssistantView): """Send a request for processing.""" hass = request.app["hass"] - intent_result = await _async_converse( - hass, data["text"], data.get("conversation_id"), self.context(request) - ) + try: + intent_result = await _async_converse( + hass, data["text"], data.get("conversation_id"), self.context(request) + ) + except intent.IntentError as err: + _LOGGER.error("Error handling intent: %s", err) + return self.json( + { + "success": False, + "error": { + "code": str(err.__class__.__name__).lower(), + "message": str(err), + }, + }, + status_code=500, + ) return self.json(intent_result) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index fc37f904e0d..7257959700f 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -13,6 +13,8 @@ from homeassistant.const import ( ATTR_NAME, CONF_ICON, CONF_NAME, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, SERVICE_RELOAD, STATE_CLOSED, STATE_HOME, @@ -134,7 +136,10 @@ def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> Lis """ found_ids: List[str] = [] for entity_id in entity_ids: - if not isinstance(entity_id, str): + if not isinstance(entity_id, str) or entity_id in ( + ENTITY_MATCH_NONE, + ENTITY_MATCH_ALL, + ): continue entity_id = entity_id.lower() diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 8aa1d7e020a..17ab9ba3b44 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -6,6 +6,7 @@ from typing import Awaitable import voluptuous as vol +from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL import homeassistant.config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, @@ -19,8 +20,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) import homeassistant.core as ha -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, intent +from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids _LOGGER = logging.getLogger(__name__) @@ -74,23 +75,16 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: await asyncio.wait(tasks) - hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) - hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) - hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on" - ) + service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA) + + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off" - ) + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}" - ) + hass.services.async_register( + ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema ) async def async_handle_core_service(call): @@ -118,6 +112,25 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: async def async_handle_update_service(call): """Service handler for updating an entity.""" + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + + if user is None: + raise UnknownUser( + context=call.context, + permission=POLICY_CONTROL, + user_id=call.context.user_id, + ) + + for entity in call.data[ATTR_ENTITY_ID]: + if not user.permissions.check_entity(entity, POLICY_CONTROL): + raise Unauthorized( + context=call.context, + permission=POLICY_CONTROL, + user_id=call.context.user_id, + perm_category=CAT_ENTITIES, + ) + tasks = [ hass.helpers.entity_component.async_update_entity(entity) for entity in call.data[ATTR_ENTITY_ID] @@ -126,13 +139,13 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: if tasks: await asyncio.wait(tasks) - hass.services.async_register( + hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service ) - hass.services.async_register( + hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service ) - hass.services.async_register( + hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service ) hass.services.async_register( diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index bdf612b2e83..37761e88347 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -5,7 +5,8 @@ import voluptuous as vol from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.core import HomeAssistant +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv, integration_platform, intent from .const import DOMAIN @@ -22,6 +23,22 @@ async def async_setup(hass: HomeAssistant, config: dict): hass, DOMAIN, _async_process_intent ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}" + ) + ) + return True diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index f84d2109095..737d99cbddd 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -201,11 +201,9 @@ async def test_toggle_intent(hass, sentence): async def test_http_api(hass, hass_client): """Test the HTTP conversation API.""" - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - result = await async_setup_component(hass, "conversation", {}) - assert result + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) client = await hass_client() hass.states.async_set("light.kitchen", "off") diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 6c2b7f78e24..0f7dc7ed309 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -4,6 +4,8 @@ import asyncio import unittest from unittest.mock import Mock, patch +import pytest +import voluptuous as vol import yaml from homeassistant import config @@ -11,9 +13,12 @@ import homeassistant.components as comps from homeassistant.components.homeassistant import ( SERVICE_CHECK_CONFIG, SERVICE_RELOAD_CORE_CONFIG, + SERVICE_SET_LOCATION, ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, @@ -24,9 +29,8 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.core as ha -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity -import homeassistant.helpers.intent as intent from homeassistant.setup import async_setup_component from tests.common import ( @@ -249,95 +253,6 @@ class TestComponentsCore(unittest.TestCase): assert not mock_stop.called -async def test_turn_on_intent(hass): - """Test HassTurnOn intent.""" - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - hass.states.async_set("light.test_light", "off") - calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - - response = await intent.async_handle( - hass, "test", "HassTurnOn", {"name": {"value": "test light"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Turned test light on" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "light" - assert call.service == "turn_on" - assert call.data == {"entity_id": ["light.test_light"]} - - -async def test_turn_off_intent(hass): - """Test HassTurnOff intent.""" - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - hass.states.async_set("light.test_light", "on") - calls = async_mock_service(hass, "light", SERVICE_TURN_OFF) - - response = await intent.async_handle( - hass, "test", "HassTurnOff", {"name": {"value": "test light"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Turned test light off" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "light" - assert call.service == "turn_off" - assert call.data == {"entity_id": ["light.test_light"]} - - -async def test_toggle_intent(hass): - """Test HassToggle intent.""" - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - hass.states.async_set("light.test_light", "off") - calls = async_mock_service(hass, "light", SERVICE_TOGGLE) - - response = await intent.async_handle( - hass, "test", "HassToggle", {"name": {"value": "test light"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Toggled test light" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "light" - assert call.service == "toggle" - assert call.data == {"entity_id": ["light.test_light"]} - - -async def test_turn_on_multiple_intent(hass): - """Test HassTurnOn intent with multiple similar entities. - - This tests that matching finds the proper entity among similar names. - """ - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - hass.states.async_set("light.test_light", "off") - hass.states.async_set("light.test_lights_2", "off") - hass.states.async_set("light.test_lighter", "off") - calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - - response = await intent.async_handle( - hass, "test", "HassTurnOn", {"name": {"value": "test lights"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Turned test lights 2 on" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "light" - assert call.service == "turn_on" - assert call.data == {"entity_id": ["light.test_lights_2"]} - - async def test_turn_on_to_not_block_for_domains_without_service(hass): """Test if turn_on is blocking domain with no service.""" await async_setup_component(hass, "homeassistant", {}) @@ -411,3 +326,49 @@ async def test_setting_location(hass): assert len(events) == 1 assert hass.config.latitude == 30 assert hass.config.longitude == 40 + + +async def test_require_admin(hass, hass_read_only_user): + """Test services requiring admin.""" + await async_setup_component(hass, "homeassistant", {}) + + for service in ( + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, + SERVICE_CHECK_CONFIG, + SERVICE_RELOAD_CORE_CONFIG, + ): + with pytest.raises(Unauthorized): + await hass.services.async_call( + ha.DOMAIN, + service, + {}, + context=ha.Context(user_id=hass_read_only_user.id), + blocking=True, + ) + assert False, f"Should have raises for {service}" + + with pytest.raises(Unauthorized): + await hass.services.async_call( + ha.DOMAIN, + SERVICE_SET_LOCATION, + {"latitude": 0, "longitude": 0}, + context=ha.Context(user_id=hass_read_only_user.id), + blocking=True, + ) + + +async def test_turn_on_off_toggle_schema(hass, hass_read_only_user): + """Test the schemas for the turn on/off/toggle services.""" + await async_setup_component(hass, "homeassistant", {}) + + for service in SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE: + for invalid in None, "nothing", ENTITY_MATCH_ALL, ENTITY_MATCH_NONE: + with pytest.raises(vol.Invalid): + await hass.services.async_call( + ha.DOMAIN, + service, + {"entity_id": invalid}, + context=ha.Context(user_id=hass_read_only_user.id), + blocking=True, + ) diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 56344b6affe..723736f35bc 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers import intent from homeassistant.setup import async_setup_component @@ -74,3 +75,96 @@ async def test_cover_intents_loading(hass): assert call.domain == "cover" assert call.service == "open_cover" assert call.data == {"entity_id": "cover.garage_door"} + + +async def test_turn_on_intent(hass): + """Test HassTurnOn intent.""" + result = await async_setup_component(hass, "homeassistant", {}) + result = await async_setup_component(hass, "intent", {}) + assert result + + hass.states.async_set("light.test_light", "off") + calls = async_mock_service(hass, "light", SERVICE_TURN_ON) + + response = await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": "test light"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Turned test light on" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "light" + assert call.service == "turn_on" + assert call.data == {"entity_id": ["light.test_light"]} + + +async def test_turn_off_intent(hass): + """Test HassTurnOff intent.""" + result = await async_setup_component(hass, "homeassistant", {}) + result = await async_setup_component(hass, "intent", {}) + assert result + + hass.states.async_set("light.test_light", "on") + calls = async_mock_service(hass, "light", SERVICE_TURN_OFF) + + response = await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": "test light"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Turned test light off" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "light" + assert call.service == "turn_off" + assert call.data == {"entity_id": ["light.test_light"]} + + +async def test_toggle_intent(hass): + """Test HassToggle intent.""" + result = await async_setup_component(hass, "homeassistant", {}) + result = await async_setup_component(hass, "intent", {}) + assert result + + hass.states.async_set("light.test_light", "off") + calls = async_mock_service(hass, "light", SERVICE_TOGGLE) + + response = await intent.async_handle( + hass, "test", "HassToggle", {"name": {"value": "test light"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Toggled test light" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "light" + assert call.service == "toggle" + assert call.data == {"entity_id": ["light.test_light"]} + + +async def test_turn_on_multiple_intent(hass): + """Test HassTurnOn intent with multiple similar entities. + + This tests that matching finds the proper entity among similar names. + """ + result = await async_setup_component(hass, "homeassistant", {}) + result = await async_setup_component(hass, "intent", {}) + assert result + + hass.states.async_set("light.test_light", "off") + hass.states.async_set("light.test_lights_2", "off") + hass.states.async_set("light.test_lighter", "off") + calls = async_mock_service(hass, "light", SERVICE_TURN_ON) + + response = await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": "test lights"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Turned test lights 2 on" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "light" + assert call.service == "turn_on" + assert call.data == {"entity_id": ["light.test_lights_2"]} From a5b4f43ea5e47307cbf7ab797b485659df3edb11 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 8 Feb 2020 13:46:13 +0100 Subject: [PATCH 155/378] Bump denonavr to 0.7.12 (#31629) --- 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 1ecbe5b939f..1387875c02d 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -2,7 +2,7 @@ "domain": "denonavr", "name": "Denon AVR Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.7.11"], + "requirements": ["denonavr==0.7.12"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c5885c8e80..5a04a4dfba9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -426,7 +426,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.7.11 +denonavr==0.7.12 # homeassistant.components.directv directpy==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e54982bf74f..7c82ade3495 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -156,7 +156,7 @@ datadog==0.15.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.7.11 +denonavr==0.7.12 # homeassistant.components.directv directpy==0.5 From ed3e16123e991a7117925232e7aeab305a0a1253 Mon Sep 17 00:00:00 2001 From: melyux <10296053+melyux@users.noreply.github.com> Date: Sat, 8 Feb 2020 05:32:52 -0800 Subject: [PATCH 156/378] Actually enable alarmdecoder to see open/close state of bypassed RF zones when armed (#31426) --- homeassistant/components/alarmdecoder/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index dc3f16b7d22..13a7913e190 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -138,7 +138,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): def _restore_callback(self, zone): """Update the zone's state, if needed.""" - if zone is None or int(zone) == self._zone_number: + if zone is None or (int(zone) == self._zone_number and not self._loop): self._state = 0 self.schedule_update_ha_state() From 83a79a434c1363c7caa7d968764e5b39fe9e0efc Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 8 Feb 2020 15:19:46 +0100 Subject: [PATCH 157/378] Use slug in ping device tracker config validation (#31329) * Use slug instead of string for config validation --- homeassistant/components/ping/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index c4d88f6061c..c0effda7a55 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -21,7 +21,7 @@ CONF_PING_COUNT = "count" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(const.CONF_HOSTS): {cv.string: cv.string}, + vol.Required(const.CONF_HOSTS): {cv.slug: cv.string}, vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int, } ) From 658c48b2370cae2ecf429c25c7f66f97660d00e8 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Sat, 8 Feb 2020 14:52:05 +0000 Subject: [PATCH 158/378] Handle missing next train from NS (#31626) * Handle missing next train * Ignore next attribute instead --- .../components/nederlandse_spoorwegen/sensor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 74b94421e3d..f4bb99399ea 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -191,10 +191,15 @@ class NSDepartureSensor(Entity): attributes["arrival_delay"] = True # Next attributes - if self._trips[1].departure_time_actual is not None: - attributes["next"] = self._trips[1].departure_time_actual.strftime("%H:%M") - elif self._trips[1].departure_time_planned is not None: - attributes["next"] = self._trips[1].departure_time_planned.strftime("%H:%M") + if len(self._trips) > 1: + if self._trips[1].departure_time_actual is not None: + attributes["next"] = self._trips[1].departure_time_actual.strftime( + "%H:%M" + ) + elif self._trips[1].departure_time_planned is not None: + attributes["next"] = self._trips[1].departure_time_planned.strftime( + "%H:%M" + ) return attributes @@ -223,7 +228,7 @@ class NSDepartureSensor(Entity): try: self._trips = self._nsapi.get_trips( - trip_time, self._departure, self._via, self._heading, True, 0, 2, + trip_time, self._departure, self._via, self._heading, True, 0, 2 ) if self._trips: if self._trips[0].departure_time_actual is None: From 6037f7364fce26c6e38c8af49b59d676b5fa65bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 8 Feb 2020 16:00:20 +0100 Subject: [PATCH 159/378] Fix hvac_action for mill (#31630) --- homeassistant/components/mill/climate.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 8f880c74c6e..d904538451c 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -6,6 +6,8 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_ON, HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -167,13 +169,20 @@ class MillHeater(ClimateDevice): """Return the maximum temperature.""" return MAX_TEMP + @property + def hvac_action(self): + """Return current hvac i.e. heat, cool, idle.""" + if self._heater.is_gen1 or self._heater.is_heating == 1: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. """ - if self._heater.is_gen1 or self._heater.is_heating == 1: + if self._heater.is_gen1 or self._heater.power_status == 1: return HVAC_MODE_HEAT return HVAC_MODE_OFF From 967b02073e99456f4e3386f0653fb284a53d4ada Mon Sep 17 00:00:00 2001 From: CHAZICLE Date: Sat, 8 Feb 2020 15:07:17 +0000 Subject: [PATCH 160/378] Remove stray debug from unifi integration (#31634) --- homeassistant/components/unifi/controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 27a0b6a668c..fc0d1324f47 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -277,7 +277,6 @@ class UniFiController: (CONF_SSID_FILTER, CONF_SSID_FILTER), ): if config in import_config: - print(config) if config == option and import_config[ config ] != self.config_entry.options.get(option): From 14e0dde05569172593d17eac1f5d972954e5839f Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 8 Feb 2020 07:58:07 -0800 Subject: [PATCH 161/378] Add Abode water valve support (#30635) --- homeassistant/components/abode/switch.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index bbe3f01f488..d6773e10ca1 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -11,6 +11,8 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE] + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode switch devices.""" @@ -18,8 +20,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - entities.append(AbodeSwitch(data, device)) + for device_type in DEVICE_TYPES: + for device in data.abode.get_devices(generic_type=device_type): + entities.append(AbodeSwitch(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): entities.append( From 1093e25a30812c45f723bf9b1cce3028049451e8 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 8 Feb 2020 18:47:54 +0100 Subject: [PATCH 162/378] Catch garmin_connect keyerrors with unknown entity type updates (#31608) * Catch keyerrors with unknown entity type updates * Change debug level and removed . from log call --- homeassistant/components/garmin_connect/const.py | 6 +++--- .../components/garmin_connect/sensor.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index e38bd72c1ee..b5faeab77b4 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -256,21 +256,21 @@ GARMIN_ENTITY_LIST = { "brpm", "mdi:progress-clock", None, - True, + False, ], "lowestRespirationValue": [ "Lowest Respiration", "brpm", "mdi:progress-clock", None, - True, + False, ], "latestRespirationValue": [ "Latest Respiration", "brpm", "mdi:progress-clock", None, - True, + False, ], "latestRespirationTimeGMT": [ "Latest Respiration Update", diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 737d53b2109..d3f95b162bf 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -165,12 +165,16 @@ class GarminConnectSensor(Entity): return data = self._data.data - if "Duration" in self._type and data[self._type]: - self._state = data[self._type] // 60 - elif "Seconds" in self._type and data[self._type]: - self._state = data[self._type] // 60 - else: - self._state = data[self._type] + try: + if "Duration" in self._type and data[self._type]: + self._state = data[self._type] // 60 + elif "Seconds" in self._type and data[self._type]: + self._state = data[self._type] // 60 + else: + self._state = data[self._type] + except KeyError: + _LOGGER.debug("Entity type %s not found in fetched data", self._type) + return _LOGGER.debug( "Entity %s set to state %s %s", self._type, self._state, self._unit From ca1319e1ef0518e4f4880aa2cecd5e357f420ad1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Feb 2020 10:56:48 -0800 Subject: [PATCH 163/378] Device tracker entities based on GPS should always publish updates (#31551) --- homeassistant/components/device_tracker/config_entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 6c5cacac591..059c51989fe 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -61,6 +61,11 @@ class BaseTrackerEntity(Entity): class TrackerEntity(BaseTrackerEntity): """Represent a tracked device.""" + @property + def force_update(self): + """All updates need to be written to the state machine.""" + return True + @property def location_accuracy(self): """Return the location accuracy of the device. From 0dd151c1c3e25d22182f9e24781610d6a290045c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Feb 2020 13:22:48 -0600 Subject: [PATCH 164/378] Resolve August integration makes too many requests and hits rate limits (#31558) --- homeassistant/components/august/__init__.py | 69 ++++++++++--------- .../components/august/binary_sensor.py | 2 +- homeassistant/components/august/camera.py | 2 +- homeassistant/components/august/lock.py | 9 ++- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- 6 files changed, 46 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 0bb0d639896..a52df5e361c 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -36,8 +36,15 @@ AUGUST_CONFIG_FILE = ".august.conf" DATA_AUGUST = "august" DOMAIN = "august" DEFAULT_ENTITY_NAMESPACE = "august" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) + +# Limit battery and hardware updates to 1800 seconds +# in order to reduce the number of api requests and +# avoid hitting rate limits +MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800) + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + LOGIN_METHODS = ["phone", "email"] CONFIG_SCHEMA = vol.Schema( @@ -180,7 +187,9 @@ class AugustData: self._access_token = access_token self._doorbells = self._api.get_doorbells(self._access_token) or [] self._locks = self._api.get_operable_locks(self._access_token) or [] - self._house_ids = [d.house_id for d in self._doorbells + self._locks] + self._house_ids = set() + for device in self._doorbells + self._locks: + self._house_ids.add(device.house_id) self._doorbell_detail_by_id = {} self._lock_status_by_id = {} @@ -284,58 +293,51 @@ class AugustData: This is the status from the door sensor. """ - self._update_doors() + self._update_locks_status() return self._door_state_by_id.get(lock_id) + def _update_locks(self): + self._update_locks_status() + self._update_locks_detail() + @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_doors(self): + def _update_locks_status(self): + status_by_id = {} state_by_id = {} - _LOGGER.debug("Start retrieving door status") + _LOGGER.debug("Start retrieving lock and door status") for lock in self._locks: - _LOGGER.debug("Updating door status for %s", lock.device_name) - + _LOGGER.debug("Updating lock and door status for %s", lock.device_name) try: - state_by_id[lock.device_id] = self._api.get_lock_door_status( - self._access_token, lock.device_id + ( + status_by_id[lock.device_id], + state_by_id[lock.device_id], + ) = self._api.get_lock_status( + self._access_token, lock.device_id, door_status=True ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door status for %s. %s", + "Request error trying to retrieve lock and door status for %s. %s", lock.device_name, ex, ) + status_by_id[lock.device_id] = None state_by_id[lock.device_id] = None except Exception: + status_by_id[lock.device_id] = None state_by_id[lock.device_id] = None raise - _LOGGER.debug("Completed retrieving door status") + _LOGGER.debug("Completed retrieving lock and door status") + self._lock_status_by_id = status_by_id self._door_state_by_id = state_by_id - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_locks(self): - status_by_id = {} + @Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES) + def _update_locks_detail(self): detail_by_id = {} - _LOGGER.debug("Start retrieving locks status") + _LOGGER.debug("Start retrieving locks detail") for lock in self._locks: - _LOGGER.debug("Updating lock status for %s", lock.device_name) - try: - status_by_id[lock.device_id] = self._api.get_lock_status( - self._access_token, lock.device_id - ) - except RequestException as ex: - _LOGGER.error( - "Request error trying to retrieve door status for %s. %s", - lock.device_name, - ex, - ) - status_by_id[lock.device_id] = None - except Exception: - status_by_id[lock.device_id] = None - raise - try: detail_by_id[lock.device_id] = self._api.get_lock_detail( self._access_token, lock.device_id @@ -351,8 +353,7 @@ class AugustData: detail_by_id[lock.device_id] = None raise - _LOGGER.debug("Completed retrieving locks status") - self._lock_status_by_id = status_by_id + _LOGGER.debug("Completed retrieving locks detail") self._lock_detail_by_id = detail_by_id def lock(self, device_id): diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 14d03189c92..f840d3db532 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -11,7 +11,7 @@ from . import DATA_AUGUST _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=10) def _retrieve_door_state(data, lock): diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 2492eb75418..885ee444c6b 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -7,7 +7,7 @@ from homeassistant.components.camera import Camera from . import DATA_AUGUST, DEFAULT_TIMEOUT -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=10) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index a541be67097..d336e21653b 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -12,7 +12,7 @@ from . import DATA_AUGUST _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=10) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -88,7 +88,12 @@ class AugustLock(LockDevice): if self._lock_detail is None: return None - return {ATTR_BATTERY_LEVEL: self._lock_detail.battery_level} + attributes = {ATTR_BATTERY_LEVEL: self._lock_detail.battery_level} + + if self._lock_detail.keypad is not None: + attributes["keypad_battery_level"] = self._lock_detail.keypad.battery_level + + return attributes @property def unique_id(self) -> str: diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e3e417d20e0..bacd7346ca7 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.7.0"], + "requirements": ["py-august==0.8.1"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 5a04a4dfba9..390ffe68a31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ pushetta==1.0.15 pwmled==1.4.1 # homeassistant.components.august -py-august==0.7.0 +py-august==0.8.1 # homeassistant.components.canary py-canary==0.5.0 From 989dd322589ae360f23fb490fff41c9f48912ac0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Feb 2020 13:20:37 -0800 Subject: [PATCH 165/378] Hue to retry if hub errors out (#31616) * Respect semaphore * Add retries when connection reset * Also catch OSError from aiohttp when authenticating --- homeassistant/components/hue/bridge.py | 40 +++++++++++++++++---- homeassistant/components/hue/const.py | 1 - homeassistant/components/hue/light.py | 18 +++++++--- homeassistant/components/hue/sensor_base.py | 2 +- tests/components/hue/test_config_flow.py | 18 +++++----- tests/components/hue/test_light.py | 4 +-- tests/components/hue/test_sensor_base.py | 4 +-- 7 files changed, 59 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index a153ed7a096..2c164e5769a 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -1,6 +1,8 @@ """Code to handle a Hue bridge.""" import asyncio +from functools import partial +from aiohttp import client_exceptions import aiohue import async_timeout import slugify as unicode_slug @@ -21,6 +23,8 @@ ATTR_SCENE_NAME = "scene_name" SCENE_SCHEMA = vol.Schema( {vol.Required(ATTR_GROUP_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string} ) +# How long should we sleep if the hub is busy +HUB_BUSY_SLEEP = 0.01 class HueBridge: @@ -101,11 +105,33 @@ class HueBridge: self.authorized = True return True - async def async_request_call(self, coro): - """Process request batched.""" + async def async_request_call(self, task): + """Limit parallel requests to Hue hub. + The Hue hub can only handle a certain amount of parallel requests, total. + Although we limit our parallel requests, we still will run into issues because + other products are hitting up Hue. + + ClientOSError means hub closed the socket on us. + ContentResponseError means hub raised an error. + Since we don't make bad requests, this is on them. + """ async with self.parallel_updates_semaphore: - return await coro + for tries in range(4): + try: + return await task() + except ( + client_exceptions.ClientOSError, + client_exceptions.ClientResponseError, + ) as err: + if tries == 3 or ( + # We only retry if it's a server error. So raise on all 4XX errors. + isinstance(err, client_exceptions.ClientResponseError) + and err.status < 500 + ): + raise + + await asyncio.sleep(HUB_BUSY_SLEEP * tries) async def async_reset(self): """Reset this bridge to default state. @@ -167,8 +193,8 @@ class HueBridge: # If we can't find it, fetch latest info. if not updated and (group is None or scene is None): - await self.api.groups.update() - await self.api.scenes.update() + await self.async_request_call(self.api.groups.update) + await self.async_request_call(self.api.scenes.update) await self.hue_activate_scene(call, updated=True) return @@ -180,7 +206,7 @@ class HueBridge: LOGGER.warning("Unable to find scene %s", scene_name) return - await group.set_action(scene=scene.id) + await self.async_request_call(partial(group.set_action, scene=scene.id)) async def handle_unauthorized_error(self): """Create a new config flow when the authorization is no longer valid.""" @@ -210,7 +236,7 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired - except (asyncio.TimeoutError, aiohue.RequestError): + except (asyncio.TimeoutError, client_exceptions.ClientOSError): raise CannotConnect except aiohue.AiohueException: LOGGER.exception("Unknown Hue linking error occurred") diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index e48cd4a8583..d8b33c60048 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -3,7 +3,6 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "hue" -API_NUPNP = "https://www.meethue.com/api/nupnp" # How long to wait to actually do the refresh after requesting it. # We wait some time so if we control multiple lights, we batch requests. diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 2e27bf65c98..14ed2483cc6 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -153,7 +153,7 @@ async def async_safe_fetch(bridge, fetch_method): """Safely fetch data.""" try: with async_timeout.timeout(4): - return await bridge.async_request_call(fetch_method()) + return await bridge.async_request_call(fetch_method) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() raise UpdateFailed @@ -376,9 +376,13 @@ class HueLight(Light): command["effect"] = "none" if self.is_group: - await self.bridge.async_request_call(self.light.set_action(**command)) + await self.bridge.async_request_call( + partial(self.light.set_action, **command) + ) else: - await self.bridge.async_request_call(self.light.set_state(**command)) + await self.bridge.async_request_call( + partial(self.light.set_state, **command) + ) await self.coordinator.async_request_refresh() @@ -401,9 +405,13 @@ class HueLight(Light): command["alert"] = "none" if self.is_group: - await self.bridge.async_request_call(self.light.set_action(**command)) + await self.bridge.async_request_call( + partial(self.light.set_action, **command) + ) else: - await self.bridge.async_request_call(self.light.set_state(**command)) + await self.bridge.async_request_call( + partial(self.light.set_state, **command) + ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 1e518c05ee5..0bc7cd53536 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -55,7 +55,7 @@ class SensorManager: try: with async_timeout.timeout(4): return await self.bridge.async_request_call( - self.bridge.api.sensors.update() + self.bridge.api.sensors.update ) except Unauthorized: await self.bridge.handle_unauthorized_error() diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 75cba40dafb..6929d6272df 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -2,7 +2,9 @@ import asyncio from unittest.mock import Mock +from aiohttp import client_exceptions import aiohue +from aiohue.discovery import URL_NUPNP from asynctest import CoroutineMock, patch import pytest import voluptuous as vol @@ -84,7 +86,7 @@ async def test_flow_works(hass): async def test_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" - aioclient_mock.get(const.API_NUPNP, json=[]) + aioclient_mock.get(URL_NUPNP, json=[]) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": "user"} @@ -95,9 +97,7 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock): async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): """Test config flow discovers only already configured bridges.""" - aioclient_mock.get( - const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] - ) + aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]) MockConfigEntry( domain="hue", unique_id="bla", data={"host": "1.2.3.4"} ).add_to_hass(hass) @@ -111,9 +111,7 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): async def test_flow_one_bridge_discovered(hass, aioclient_mock): """Test config flow discovers one bridge.""" - aioclient_mock.get( - const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] - ) + aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": "user"} @@ -130,7 +128,7 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): ).add_to_hass(hass) aioclient_mock.get( - const.API_NUPNP, + URL_NUPNP, json=[ {"internalipaddress": "1.2.3.4", "id": "bla"}, {"internalipaddress": "5.6.7.8", "id": "beer"}, @@ -153,7 +151,7 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): """Test config flow discovers two bridges.""" aioclient_mock.get( - const.API_NUPNP, + URL_NUPNP, json=[ {"internalipaddress": "1.2.3.4", "id": "bla"}, {"internalipaddress": "5.6.7.8", "id": "beer"}, @@ -259,7 +257,7 @@ async def test_flow_link_button_not_pressed(hass): async def test_flow_link_unknown_host(hass): """Test config flow .""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=aiohue.RequestError), + mock_create_user=CoroutineMock(side_effect=client_exceptions.ClientOSError), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index d57c15bfa36..de50d23b947 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -206,8 +206,8 @@ def mock_bridge(hass): return bridge.mock_group_responses.popleft() return None - async def async_request_call(coro): - await coro + async def async_request_call(task): + await task() bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 78255116831..ca83da725fa 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -279,8 +279,8 @@ def create_mock_bridge(hass): return bridge.mock_sensor_responses.popleft() return None - async def async_request_call(coro): - await coro + async def async_request_call(task): + await task() bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" From e3894d212c8d7b2ab36e33b4911460352625e64a Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 8 Feb 2020 16:34:22 -0500 Subject: [PATCH 166/378] Bump insteonplm to 0.16.7 (#31645) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 74d8274796b..69c35477b8d 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["insteonplm==0.16.6"], + "requirements": ["insteonplm==0.16.7"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 390ffe68a31..036cce48405 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ incomfort-client==0.4.0 influxdb==5.2.3 # homeassistant.components.insteon -insteonplm==0.16.6 +insteonplm==0.16.7 # homeassistant.components.iperf3 iperf3==0.1.11 From 9f58e5d6eaed731be728596a5b06e981f604fe30 Mon Sep 17 00:00:00 2001 From: Bruno Furtado Date: Sun, 9 Feb 2020 00:17:41 +0000 Subject: [PATCH 167/378] Only auth on enter_learning in response to errors for broadlink (#27341) * Only auth on enter_learning in response to errors. * Remove extra newline. * Add missing break on successful attempt. * Avoid logging success message when auth is unsuccessful. --- .../components/broadlink/__init__.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index dd7c02b82ad..be6aa266491 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -75,18 +75,20 @@ def async_setup_service(hass, host, device): async def _learn_command(call): """Learn a packet from remote.""" + device = hass.data[DOMAIN][call.data[CONF_HOST]] - try: - auth = await hass.async_add_executor_job(device.auth) - except socket.timeout: - _LOGGER.error("Failed to connect to device, timeout") - return - if not auth: - _LOGGER.error("Failed to connect to device") - return - - await hass.async_add_executor_job(device.enter_learning) + for retry in range(DEFAULT_RETRY): + try: + await hass.async_add_executor_job(device.enter_learning) + break + except (socket.timeout, ValueError): + try: + await hass.async_add_executor_job(device.auth) + except socket.timeout: + if retry == DEFAULT_RETRY - 1: + _LOGGER.error("Failed to enter learning mode") + return _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() From a2bea2cab83195f7c96d7ad354b8b27632a21484 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 9 Feb 2020 00:31:39 +0000 Subject: [PATCH 168/378] [ci skip] Translation update --- .../components/abode/.translations/pl.json | 2 +- .../ambient_station/.translations/pl.json | 2 +- .../components/axis/.translations/pl.json | 6 ++--- .../cert_expiry/.translations/pl.json | 4 ++-- .../components/daikin/.translations/pl.json | 2 +- .../components/deconz/.translations/pl.json | 4 ++-- .../emulated_roku/.translations/pl.json | 2 +- .../components/esphome/.translations/pl.json | 2 +- .../components/gdacs/.translations/no.json | 11 +++++++++ .../geonetnz_quakes/.translations/pl.json | 2 +- .../geonetnz_volcano/.translations/pl.json | 2 +- .../components/hangouts/.translations/pl.json | 2 +- .../homekit_controller/.translations/pl.json | 2 +- .../homematicip_cloud/.translations/pl.json | 2 +- .../huawei_lte/.translations/pl.json | 4 ++-- .../components/hue/.translations/pl.json | 6 ++--- .../iaqualink/.translations/pl.json | 2 +- .../components/icloud/.translations/pl.json | 2 +- .../components/ipma/.translations/pl.json | 2 +- .../components/iqvia/.translations/pl.json | 2 +- .../components/life360/.translations/pl.json | 4 ++-- .../components/linky/.translations/pl.json | 2 +- .../components/local_ip/.translations/pl.json | 2 +- .../luftdaten/.translations/pl.json | 2 +- .../components/met/.translations/pl.json | 2 +- .../meteo_france/.translations/pl.json | 2 +- .../components/mikrotik/.translations/pl.json | 4 ++-- .../minecraft_server/.translations/da.json | 24 +++++++++++++++++++ .../minecraft_server/.translations/en.json | 24 +++++++++++++++++++ .../minecraft_server/.translations/ko.json | 24 +++++++++++++++++++ .../minecraft_server/.translations/no.json | 16 +++++++++++++ .../minecraft_server/.translations/ru.json | 24 +++++++++++++++++++ .../components/neato/.translations/pl.json | 2 +- .../components/notion/.translations/pl.json | 6 ++--- .../opentherm_gw/.translations/pl.json | 4 ++-- .../components/openuv/.translations/pl.json | 2 +- .../components/plex/.translations/pl.json | 2 +- .../rainmachine/.translations/pl.json | 2 +- .../components/ring/.translations/pl.json | 2 +- .../components/sentry/.translations/pl.json | 2 +- .../simplisafe/.translations/pl.json | 2 +- .../components/smhi/.translations/pl.json | 2 +- .../solaredge/.translations/pl.json | 4 ++-- .../components/solarlog/.translations/pl.json | 4 ++-- .../tellduslive/.translations/pl.json | 2 +- .../components/tesla/.translations/pl.json | 2 +- .../components/tradfri/.translations/pl.json | 2 +- .../transmission/.translations/pl.json | 2 +- .../twentemilieu/.translations/pl.json | 6 ++--- .../components/unifi/.translations/pl.json | 2 +- .../components/upnp/.translations/pl.json | 2 +- .../components/velbus/.translations/pl.json | 4 ++-- .../components/wled/.translations/pl.json | 2 +- .../components/wwlln/.translations/pl.json | 2 +- .../components/zone/.translations/pl.json | 2 +- .../components/zwave/.translations/pl.json | 2 +- 56 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 homeassistant/components/gdacs/.translations/no.json create mode 100644 homeassistant/components/minecraft_server/.translations/da.json create mode 100644 homeassistant/components/minecraft_server/.translations/en.json create mode 100644 homeassistant/components/minecraft_server/.translations/ko.json create mode 100644 homeassistant/components/minecraft_server/.translations/no.json create mode 100644 homeassistant/components/minecraft_server/.translations/ru.json diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json index c3f3b8f2c88..d086aaca395 100644 --- a/homeassistant/components/abode/.translations/pl.json +++ b/homeassistant/components/abode/.translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.", - "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json index 6ebd0848a63..3ac612d0ea7 100644 --- a/homeassistant/components/ambient_station/.translations/pl.json +++ b/homeassistant/components/ambient_station/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany", + "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany.", "invalid_key": "Nieprawid\u0142owy klucz API i/lub klucz aplikacji", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index d5deb327a75..9f7bb55147d 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego", "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis", "updated_configuration": "Zaktualizowano konfiguracj\u0119 urz\u0105dzenia o nowy adres hosta" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index 671cbfcd1ff..2e50a9f8cbc 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" + "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany." }, "error": { "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.", - "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", + "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.", "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", "wrong_host": "Certyfikat nie pasuje do nazwy hosta" }, diff --git a/homeassistant/components/daikin/.translations/pl.json b/homeassistant/components/daikin/.translations/pl.json index 5d5448a93db..3caea70c4de 100644 --- a/homeassistant/components/daikin/.translations/pl.json +++ b/homeassistant/components/daikin/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", "device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem." }, diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index df85e7b8d1d..65e858a626d 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Mostek jest ju\u017c skonfigurowany", - "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.", + "already_configured": "Mostek jest ju\u017c skonfigurowany.", + "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", "not_deconz_bridge": "To nie jest mostek deCONZ", "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ", diff --git a/homeassistant/components/emulated_roku/.translations/pl.json b/homeassistant/components/emulated_roku/.translations/pl.json index 0ed3cc3d14a..0dd32f66c9f 100644 --- a/homeassistant/components/emulated_roku/.translations/pl.json +++ b/homeassistant/components/emulated_roku/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Nazwa ju\u017c istnieje." }, "step": { "user": { diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json index 9394b5af543..ebd201b5550 100644 --- a/homeassistant/components/esphome/.translations/pl.json +++ b/homeassistant/components/esphome/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "ESP jest ju\u017c skonfigurowane" + "already_configured": "ESP jest ju\u017c skonfigurowane." }, "error": { "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", diff --git a/homeassistant/components/gdacs/.translations/no.json b/homeassistant/components/gdacs/.translations/no.json new file mode 100644 index 00000000000..c06ad6378b5 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/no.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index fd82bba43b5..bdd8f152d39 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json index 7d329815f3f..c51a69356a1 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/pl.json +++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json index 5da1e219799..1d08296007a 100644 --- a/homeassistant/components/hangouts/.translations/pl.json +++ b/homeassistant/components/hangouts/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts jest ju\u017c skonfigurowany", + "already_configured": "Google Hangouts jest ju\u017c skonfigurowany.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d." }, "error": { diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json index e66353c5000..33cd20dc9c9 100644 --- a/homeassistant/components/homekit_controller/.translations/pl.json +++ b/homeassistant/components/homekit_controller/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Nie mo\u017cna rozpocz\u0105\u0107 parowania, poniewa\u017c nie znaleziono urz\u0105dzenia.", "already_configured": "Akcesorium jest ju\u017c skonfigurowane z tym kontrolerem.", - "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.", + "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "already_paired": "To akcesorium jest ju\u017c sparowane z innym urz\u0105dzeniem. Zresetuj akcesorium i spr\u00f3buj ponownie.", "ignored_model": "Obs\u0142uga HomeKit dla tego modelu jest zablokowana, poniewa\u017c dost\u0119pna jest pe\u0142niejsza integracja natywna.", "invalid_config_entry": "To urz\u0105dzenie jest wy\u015bwietlane jako gotowe do sparowania, ale istnieje ju\u017c konfliktowy wpis konfiguracyjny dla niego w Home Assistant, kt\u00f3ry musi zosta\u0107 najpierw usuni\u0119ty.", diff --git a/homeassistant/components/homematicip_cloud/.translations/pl.json b/homeassistant/components/homematicip_cloud/.translations/pl.json index 7c8714c2c11..78905da208e 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pl.json +++ b/homeassistant/components/homematicip_cloud/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany", + "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.", "connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index a4e7d72852a..4029b24df3f 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 3866af9d7fc..00b9374459c 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", - "already_configured": "Mostek jest ju\u017c skonfigurowany", - "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.", + "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane.", + "already_configured": "Mostek jest ju\u017c skonfigurowany.", + "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue", diff --git a/homeassistant/components/iaqualink/.translations/pl.json b/homeassistant/components/iaqualink/.translations/pl.json index 211a65f5ccb..d14a2775c15 100644 --- a/homeassistant/components/iaqualink/.translations/pl.json +++ b/homeassistant/components/iaqualink/.translations/pl.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika / adres e-mail" + "username": "Nazwa u\u017cytkownika/adres e-mail" }, "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.", "title": "Po\u0142\u0105cz z iAqualink" diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json index 169fe2eac2d..41e182eceee 100644 --- a/homeassistant/components/icloud/.translations/pl.json +++ b/homeassistant/components/icloud/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", diff --git a/homeassistant/components/ipma/.translations/pl.json b/homeassistant/components/ipma/.translations/pl.json index 735f5a4a126..7eb8055e1a1 100644 --- a/homeassistant/components/ipma/.translations/pl.json +++ b/homeassistant/components/ipma/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Nazwa ju\u017c istnieje." }, "step": { "user": { diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json index b528cdeb70f..b8c014c3dc9 100644 --- a/homeassistant/components/iqvia/.translations/pl.json +++ b/homeassistant/components/iqvia/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Kod pocztowy ju\u017c zarejestrowany", + "identifier_exists": "Kod pocztowy jest ju\u017c zarejestrowany.", "invalid_zip_code": "Kod pocztowy jest nieprawid\u0142owy" }, "step": { diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index e9cd9920304..f82c8325828 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto jest ju\u017c skonfigurowane" + "user_already_configured": "Konto jest ju\u017c skonfigurowane." }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360", - "user_already_configured": "Konto jest ju\u017c skonfigurowane" + "user_already_configured": "Konto jest ju\u017c skonfigurowane." }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index 51f96dcf17a..62da10e1c96 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe", diff --git a/homeassistant/components/local_ip/.translations/pl.json b/homeassistant/components/local_ip/.translations/pl.json index a4032eeebd1..82b614a8e17 100644 --- a/homeassistant/components/local_ip/.translations/pl.json +++ b/homeassistant/components/local_ip/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie" + "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie." }, "step": { "user": { diff --git a/homeassistant/components/luftdaten/.translations/pl.json b/homeassistant/components/luftdaten/.translations/pl.json index 5a2c30db44c..19e71b5156f 100644 --- a/homeassistant/components/luftdaten/.translations/pl.json +++ b/homeassistant/components/luftdaten/.translations/pl.json @@ -3,7 +3,7 @@ "error": { "communication_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z API Luftdaten", "invalid_sensor": "Sensor niedost\u0119pny lub nieprawid\u0142owy", - "sensor_exists": "Sensor zosta\u0142 ju\u017c zarejestrowany" + "sensor_exists": "Sensor zosta\u0142 ju\u017c zarejestrowany." }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index f647dcf7b45..e22ac763d56 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Lokalizacja ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje." }, "step": { "user": { diff --git a/homeassistant/components/meteo_france/.translations/pl.json b/homeassistant/components/meteo_france/.translations/pl.json index a519eaead5d..38aa1944fac 100644 --- a/homeassistant/components/meteo_france/.translations/pl.json +++ b/homeassistant/components/meteo_france/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Miasto jest ju\u017c skonfigurowane", + "already_configured": "Miasto jest ju\u017c skonfigurowane.", "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej" }, "step": { diff --git a/homeassistant/components/mikrotik/.translations/pl.json b/homeassistant/components/mikrotik/.translations/pl.json index 1971f1866e1..1ca6fa7d1bb 100644 --- a/homeassistant/components/mikrotik/.translations/pl.json +++ b/homeassistant/components/mikrotik/.translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Mikronik jest ju\u017c skonfigurowany" + "already_configured": "Mikronik jest ju\u017c skonfigurowany." }, "error": { "cannot_connect": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", - "name_exists": "Nazwa ju\u017c istnieje", + "name_exists": "Nazwa ju\u017c istnieje.", "wrong_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/minecraft_server/.translations/da.json b/homeassistant/components/minecraft_server/.translations/da.json new file mode 100644 index 00000000000..bf930f2f277 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret." + }, + "error": { + "cannot_connect": "Det lykkedes ikke at oprette forbindelse til serveren. Kontroller v\u00e6rten og porten, og pr\u00f8v igen. S\u00f8rg ogs\u00e5 for, at du k\u00f8rer mindst Minecraft version 1.7 p\u00e5 din server.", + "invalid_ip": "IP-adressen er ugyldig (MAC-adressen kunne ikke bestemmes). Ret den, og pr\u00f8v igen.", + "invalid_port": "Porten skal v\u00e6re i intervallet fra 1024 til 65535. Ret den, og pr\u00f8v igen." + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "port": "Port" + }, + "description": "Konfigurer din Minecraft-server-instans for at tillade overv\u00e5gning.", + "title": "Forbind din Minecraft-server" + } + }, + "title": "Minecraft-server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/en.json b/homeassistant/components/minecraft_server/.translations/en.json new file mode 100644 index 00000000000..d0f7a5d6300 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Host is already configured." + }, + "error": { + "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.", + "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again.", + "invalid_port": "Port must be in range from 1024 to 65535. Please correct it and try again." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + }, + "description": "Set up your Minecraft Server instance to allow monitoring.", + "title": "Link your Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/ko.json b/homeassistant/components/minecraft_server/.translations/ko.json new file mode 100644 index 00000000000..66b281cc5d9 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \ub610\ud55c \uc11c\ubc84\uc5d0\uc11c Minecraft \ubc84\uc804 1.7 \uc774\uc0c1\uc744 \uc2e4\ud589 \uc911\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (MAC \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4). \uc218\uc815 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_port": "\ud3ec\ud2b8\ub294 1024-65535 \ubc94\uc704\uc5d0 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud3ec\ud2b8\ub97c \uc218\uc815\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "port": "\ud3ec\ud2b8" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d Minecraft \uc11c\ubc84 \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "Minecraft \uc11c\ubc84 \uc5f0\uacb0" + } + }, + "title": "Minecraft \uc11c\ubc84" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json new file mode 100644 index 00000000000..aebd8c59136 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Verten er allerede konfigurert." + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/ru.json b/homeassistant/components/minecraft_server/.translations/ru.json new file mode 100644 index 00000000000..916b342ee4a --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0422\u0430\u043a\u0436\u0435 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d Minecraft \u0432\u0435\u0440\u0441\u0438\u0438 1.7, \u0438\u043b\u0438 \u0432\u044b\u0448\u0435.", + "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c MAC-\u0430\u0434\u0440\u0435\u0441).", + "invalid_port": "\u041f\u043e\u0440\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 \u043e\u0442 1024 \u0434\u043e 65535." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.", + "title": "Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json index caea115b7d5..e6b55b12c53 100644 --- a/homeassistant/components/neato/.translations/pl.json +++ b/homeassistant/components/neato/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_configured": "Konto jest ju\u017c skonfigurowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "create_entry": { diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index 380d4ad151e..ffb3b8386dd 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Nazwa u\u017cytkownika ju\u017c zarejestrowana", + "identifier_exists": "Nazwa u\u017cytkownika jest ju\u017c zarejestrowana.", "invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, @@ -9,11 +9,11 @@ "user": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika / adres e-mail" + "username": "Nazwa u\u017cytkownika/adres e-mail" }, "title": "Wprowad\u017a dane" } }, - "title": "Poj\u0119cie" + "title": "Notion" } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index fe8ccfc8975..88791781e3f 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "error": { - "already_configured": "Bramka jest ju\u017c skonfigurowana", - "id_exists": "Identyfikator bramki ju\u017c istnieje", + "already_configured": "Bramka jest ju\u017c skonfigurowana.", + "id_exists": "Identyfikator bramki ju\u017c istnieje.", "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem", "timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia." }, diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json index ee3875c2903..ff6d1b21055 100644 --- a/homeassistant/components/openuv/.translations/pl.json +++ b/homeassistant/components/openuv/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane", + "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane.", "invalid_api_key": "Nieprawid\u0142owy klucz API" }, "step": { diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index d752899b9f0..d9ab9db8bc9 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", - "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", + "already_configured": "Ten serwer Plex jest ju\u017c skonfigurowany.", "already_in_progress": "Plex jest konfigurowany", "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index cf842efe9f6..d5b853122a6 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Konto jest ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/ring/.translations/pl.json b/homeassistant/components/ring/.translations/pl.json index f34903ff7d1..e592522c43b 100644 --- a/homeassistant/components/ring/.translations/pl.json +++ b/homeassistant/components/ring/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", diff --git a/homeassistant/components/sentry/.translations/pl.json b/homeassistant/components/sentry/.translations/pl.json index 4bb7abbc328..d97fa159a87 100644 --- a/homeassistant/components/sentry/.translations/pl.json +++ b/homeassistant/components/sentry/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Sentry jest ju\u017c skonfigurowane" + "already_configured": "Sentry jest ju\u017c skonfigurowane." }, "error": { "bad_dsn": "Nieprawid\u0142owy DSN", diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index ad8a15d06b7..71316eb1a6c 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Konto jest ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/smhi/.translations/pl.json b/homeassistant/components/smhi/.translations/pl.json index 21973cd54b6..818f27853ff 100644 --- a/homeassistant/components/smhi/.translations/pl.json +++ b/homeassistant/components/smhi/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje", + "name_exists": "Nazwa ju\u017c istnieje.", "wrong_location": "Lokalizacja w Szwecji" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/pl.json b/homeassistant/components/solaredge/.translations/pl.json index 376a81219b0..5e80c1563f4 100644 --- a/homeassistant/components/solaredge/.translations/pl.json +++ b/homeassistant/components/solaredge/.translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + "site_exists": "To site_id jest ju\u017c skonfigurowane." }, "error": { - "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + "site_exists": "To site_id jest ju\u017c skonfigurowane." }, "step": { "user": { diff --git a/homeassistant/components/solarlog/.translations/pl.json b/homeassistant/components/solarlog/.translations/pl.json index 251d183b361..fdbf21feb92 100644 --- a/homeassistant/components/solarlog/.translations/pl.json +++ b/homeassistant/components/solarlog/.translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" }, "step": { diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 01d3c7125c3..68e53df57f1 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "TelldusLive jest ju\u017c skonfigurowany", + "already_setup": "TelldusLive jest ju\u017c skonfigurowany.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" diff --git a/homeassistant/components/tesla/.translations/pl.json b/homeassistant/components/tesla/.translations/pl.json index 5a8a3d2ebd3..89233646ef0 100644 --- a/homeassistant/components/tesla/.translations/pl.json +++ b/homeassistant/components/tesla/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie", - "identifier_exists": "Adres e-mail ju\u017c zarejestrowany", + "identifier_exists": "Adres e-mail jest ju\u017c zarejestrowany.", "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia", "unknown_error": "Nieznany b\u0142\u0105d, prosz\u0119 zg\u0142osi\u0107 dane z loga" }, diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index fc115294031..208687839dd 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Mostek jest ju\u017c skonfigurowany", + "already_configured": "Mostek jest ju\u017c skonfigurowany.", "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku." }, "error": { diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json index a85a3f9b006..5aac538766b 100644 --- a/homeassistant/components/transmission/.translations/pl.json +++ b/homeassistant/components/transmission/.translations/pl.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", - "name_exists": "Nazwa ju\u017c istnieje", + "name_exists": "Nazwa ju\u017c istnieje.", "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" }, "step": { diff --git a/homeassistant/components/twentemilieu/.translations/pl.json b/homeassistant/components/twentemilieu/.translations/pl.json index 042fcf0dda6..130672906ef 100644 --- a/homeassistant/components/twentemilieu/.translations/pl.json +++ b/homeassistant/components/twentemilieu/.translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "address_exists": "Adres ju\u017c skonfigurowany." + "address_exists": "Adres jest ju\u017c skonfigurowany." }, "error": { - "connection_error": "Po\u0142\u0105czenie nieudane.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." }, "step": { "user": { "data": { - "house_letter": "List domowy / dodatkowy", + "house_letter": "List domowy/dodatkowy", "house_number": "Numer domu", "post_code": "Kod pocztowy" }, diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 5887460a8a5..9c71279c444 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana", + "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana.", "user_privilege": "U\u017cytkownik musi by\u0107 administratorem" }, "error": { diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json index d7ede44d22d..964e5a6818d 100644 --- a/homeassistant/components/upnp/.translations/pl.json +++ b/homeassistant/components/upnp/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane", + "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane.", "incomplete_device": "Ignorowanie niekompletnego urz\u0105dzenia UPnP", "no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD", "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 UPnP/IGD.", diff --git a/homeassistant/components/velbus/.translations/pl.json b/homeassistant/components/velbus/.translations/pl.json index 72e18b0e2c8..0856d142bef 100644 --- a/homeassistant/components/velbus/.translations/pl.json +++ b/homeassistant/components/velbus/.translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "port_exists": "Ten port jest ju\u017c skonfigurowany" + "port_exists": "Ten port jest ju\u017c skonfigurowany." }, "error": { "connection_failed": "Po\u0142\u0105czenie Velbus nie powiod\u0142o si\u0119", - "port_exists": "Ten port jest ju\u017c skonfigurowany" + "port_exists": "Ten port jest ju\u017c skonfigurowany." }, "step": { "user": { diff --git a/homeassistant/components/wled/.translations/pl.json b/homeassistant/components/wled/.translations/pl.json index c10c8ab34d6..6080336c44f 100644 --- a/homeassistant/components/wled/.translations/pl.json +++ b/homeassistant/components/wled/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane", + "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane.", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED." }, "error": { diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index 652d580644f..658dbebbe45 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json index e649de4c75e..5c013d5da8f 100644 --- a/homeassistant/components/zone/.translations/pl.json +++ b/homeassistant/components/zone/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Nazwa ju\u017c istnieje." }, "step": { "init": { diff --git a/homeassistant/components/zwave/.translations/pl.json b/homeassistant/components/zwave/.translations/pl.json index 254008ddb4c..a985405c009 100644 --- a/homeassistant/components/zwave/.translations/pl.json +++ b/homeassistant/components/zwave/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Z-Wave jest ju\u017c skonfigurowany", + "already_configured": "Z-Wave jest ju\u017c skonfigurowany.", "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 Z-Wave" }, "error": { From 9987978d1a71f8c2e1828bceb57188fc0e1bd7ef Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Sun, 9 Feb 2020 00:45:28 +0000 Subject: [PATCH 169/378] Add unique ID to edimax switches (#27984) * Add unique ID and device info data * Don't get power info on switch models lacking it * Move info fetching to update, update before adding * Upgrade pyedimax to get device info * Remove device info Co-authored-by: Paulus Schoutsen --- homeassistant/components/edimax/manifest.json | 2 +- homeassistant/components/edimax/switch.py | 34 ++++++++++++++----- requirements_all.txt | 2 +- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 20036311592..de8b978b9f9 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -2,7 +2,7 @@ "domain": "edimax", "name": "Edimax", "documentation": "https://www.home-assistant.io/integrations/edimax", - "requirements": ["pyedimax==0.1"], + "requirements": ["pyedimax==0.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 3d558f6c770..e44ec23bca7 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -10,6 +10,8 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DOMAIN = "edimax" + DEFAULT_NAME = "Edimax Smart Plug" DEFAULT_PASSWORD = "1234" DEFAULT_USERNAME = "admin" @@ -30,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) name = config.get(CONF_NAME) - add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)]) + add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)], True) class SmartPlugSwitch(SwitchDevice): @@ -43,6 +45,14 @@ class SmartPlugSwitch(SwitchDevice): self._now_power = None self._now_energy_day = None self._state = False + self._supports_power_monitoring = False + self._info = None + self._mac = None + + @property + def unique_id(self): + """Return the device's MAC address.""" + return self._mac @property def name(self): @@ -74,14 +84,20 @@ class SmartPlugSwitch(SwitchDevice): def update(self): """Update edimax switch.""" - try: - self._now_power = float(self.smartplug.now_power) - except (TypeError, ValueError): - self._now_power = None + if not self._info: + self._info = self.smartplug.info + self._mac = self._info["mac"] + self._supports_power_monitoring = self._info["model"] != "SP1101W" - try: - self._now_energy_day = float(self.smartplug.now_energy_day) - except (TypeError, ValueError): - self._now_energy_day = None + if self._supports_power_monitoring: + try: + self._now_power = float(self.smartplug.now_power) + except (TypeError, ValueError): + self._now_power = None + + try: + self._now_energy_day = float(self.smartplug.now_energy_day) + except (TypeError, ValueError): + self._now_energy_day = None self._state = self.smartplug.state == "ON" diff --git a/requirements_all.txt b/requirements_all.txt index 036cce48405..159e041b731 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ pyebox==1.1.4 pyeconet==0.0.11 # homeassistant.components.edimax -pyedimax==0.1 +pyedimax==0.2.1 # homeassistant.components.eight_sleep pyeight==0.1.2 From da0ddc84ab27dff707bb77ddd22b4ce00acae5cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Feb 2020 17:26:58 -0800 Subject: [PATCH 170/378] Guard writing automation/scene/script config (#31568) --- homeassistant/components/config/__init__.py | 28 ++++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index ad7ae14ecb7..682e23dd14c 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -94,6 +94,7 @@ class BaseEditConfigView(HomeAssistantView): self.data_schema = data_schema self.post_write_hook = post_write_hook self.data_validator = data_validator + self.mutation_lock = asyncio.Lock() def _empty_config(self): """Empty config if file not found.""" @@ -114,8 +115,9 @@ class BaseEditConfigView(HomeAssistantView): async def get(self, request, config_key): """Fetch device specific config.""" hass = request.app["hass"] - current = await self.read_config(hass) - value = self._get_value(hass, current, config_key) + async with self.mutation_lock: + current = await self.read_config(hass) + value = self._get_value(hass, current, config_key) if value is None: return self.json_message("Resource not found", 404) @@ -148,10 +150,11 @@ class BaseEditConfigView(HomeAssistantView): path = hass.config.path(self.path) - current = await self.read_config(hass) - self._write_value(hass, current, config_key, data) + async with self.mutation_lock: + current = await self.read_config(hass) + self._write_value(hass, current, config_key, data) - await hass.async_add_executor_job(_write, path, current) + await hass.async_add_executor_job(_write, path, current) if self.post_write_hook is not None: hass.async_create_task( @@ -163,15 +166,16 @@ class BaseEditConfigView(HomeAssistantView): async def delete(self, request, config_key): """Remove an entry.""" hass = request.app["hass"] - current = await self.read_config(hass) - value = self._get_value(hass, current, config_key) - path = hass.config.path(self.path) + async with self.mutation_lock: + current = await self.read_config(hass) + value = self._get_value(hass, current, config_key) + path = hass.config.path(self.path) - if value is None: - return self.json_message("Resource not found", 404) + if value is None: + return self.json_message("Resource not found", 404) - self._delete_value(hass, current, config_key) - await hass.async_add_executor_job(_write, path, current) + self._delete_value(hass, current, config_key) + await hass.async_add_executor_job(_write, path, current) if self.post_write_hook is not None: hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key)) From 645c673720d412bcb0e12764269ae86d2021d608 Mon Sep 17 00:00:00 2001 From: crallian Date: Sun, 9 Feb 2020 17:41:43 +0100 Subject: [PATCH 171/378] Added zone type Technical as power. (#31611) *The zone type technical can be used in SPC to track status of e.g. mapping keys and outputs. --- homeassistant/components/spc/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index b5ff14ce01d..34689c4dccf 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -19,6 +19,7 @@ def _get_device_class(zone_type): ZoneType.ALARM: "motion", ZoneType.ENTRY_EXIT: "opening", ZoneType.FIRE: "smoke", + ZoneType.TECHNICAL: "power", }.get(zone_type) From 150b376cf90f60c08ace286f5f08549933e03ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20=C3=98strup?= <7877957+TechnicallyJoe@users.noreply.github.com> Date: Sun, 9 Feb 2020 17:43:09 +0100 Subject: [PATCH 172/378] Add recorder vars db_max_retries and db_retry_wait (#31561) * added recorder vars db_max_retries and db_retry_wait * fixed test_recorder_setup_failure I failed because it was missing the two new variables. I simply added these with default values. * fixed syntax error in test_recorder_setup_failure * fixed formatting error in test_init_py for recorder component * fixed typo in test case * Updated the way the default keys for db_,max_wait and db_retry_wait is set Implemented based on suggestions from @springstan * Updated config_schema call to adhere to Black * changed conf.get to conf[dict] for var retrieval * removed 2 blank lines --- homeassistant/components/recorder/__init__.py | 32 ++++++++++++++----- tests/components/recorder/test_init.py | 9 +++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index ab56a5fc33b..af34d4dd9f6 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -59,14 +59,16 @@ SERVICE_PURGE_SCHEMA = vol.Schema( DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" +DEFAULT_DB_MAX_RETRIES = 10 +DEFAULT_DB_RETRY_WAIT = 3 CONF_DB_URL = "db_url" +CONF_DB_MAX_RETRIES = "db_max_retries" +CONF_DB_RETRY_WAIT = "db_retry_wait" CONF_PURGE_KEEP_DAYS = "purge_keep_days" CONF_PURGE_INTERVAL = "purge_interval" CONF_EVENT_TYPES = "event_types" -CONNECT_RETRY_WAIT = 3 - FILTER_SCHEMA = vol.Schema( { vol.Optional(CONF_EXCLUDE, default={}): vol.Schema( @@ -96,6 +98,12 @@ CONFIG_SCHEMA = vol.Schema( vol.Coerce(int), vol.Range(min=0) ), vol.Optional(CONF_DB_URL): cv.string, + vol.Optional( + CONF_DB_MAX_RETRIES, default=DEFAULT_DB_MAX_RETRIES + ): cv.positive_int, + vol.Optional( + CONF_DB_RETRY_WAIT, default=DEFAULT_DB_RETRY_WAIT + ): cv.positive_int, } ) }, @@ -133,6 +141,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = config[DOMAIN] keep_days = conf.get(CONF_PURGE_KEEP_DAYS) purge_interval = conf.get(CONF_PURGE_INTERVAL) + db_max_retries = conf[CONF_DB_MAX_RETRIES] + db_retry_wait = conf[CONF_DB_RETRY_WAIT] db_url = conf.get(CONF_DB_URL, None) if not db_url: @@ -145,6 +155,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_days=keep_days, purge_interval=purge_interval, uri=db_url, + db_max_retries=db_max_retries, + db_retry_wait=db_retry_wait, include=include, exclude=exclude, ) @@ -174,6 +186,8 @@ class Recorder(threading.Thread): keep_days: int, purge_interval: int, uri: str, + db_max_retries: int, + db_retry_wait: int, include: Dict, exclude: Dict, ) -> None: @@ -186,6 +200,8 @@ class Recorder(threading.Thread): self.queue: Any = queue.Queue() self.recording_start = dt_util.utcnow() self.db_url = uri + self.db_max_retries = db_max_retries + self.db_retry_wait = db_retry_wait self.async_db_ready = asyncio.Future() self.engine: Any = None self.run_info: Any = None @@ -217,9 +233,9 @@ class Recorder(threading.Thread): tries = 1 connected = False - while not connected and tries <= 10: + while not connected and tries <= self.db_max_retries: if tries != 1: - time.sleep(CONNECT_RETRY_WAIT) + time.sleep(self.db_retry_wait) try: self._setup_connection() migration.migrate_schema(self) @@ -230,7 +246,7 @@ class Recorder(threading.Thread): _LOGGER.error( "Error during connection setup: %s (retrying in %s seconds)", err, - CONNECT_RETRY_WAIT, + self.db_retry_wait, ) tries += 1 @@ -337,9 +353,9 @@ class Recorder(threading.Thread): tries = 1 updated = False - while not updated and tries <= 10: + while not updated and tries <= self.db_max_retries: if tries != 1: - time.sleep(CONNECT_RETRY_WAIT) + time.sleep(self.db_retry_wait) try: with session_scope(session=self.get_session()) as session: try: @@ -367,7 +383,7 @@ class Recorder(threading.Thread): "Error in database connectivity: %s. " "(retrying in %s seconds)", err, - CONNECT_RETRY_WAIT, + self.db_retry_wait, ) tries += 1 diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index ae04066651f..a21ef578ca9 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -198,7 +198,14 @@ def test_recorder_setup_failure(): ): setup.side_effect = ImportError("driver not found") rec = Recorder( - hass, keep_days=7, purge_interval=2, uri="sqlite://", include={}, exclude={} + hass, + keep_days=7, + purge_interval=2, + uri="sqlite://", + db_max_retries=10, + db_retry_wait=3, + include={}, + exclude={}, ) rec.start() rec.join() From fb2e120563d3e1216f86193171741ebc7e09fab6 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 9 Feb 2020 17:46:00 +0100 Subject: [PATCH 173/378] Sure Petcare new features various improvements (#31437) * add typing * 100% battery_level is enough * human-friendly datetime * better enum usage * add online and learning mode attrs * use max two decimals in attrs * use legacy style debug logging * remove str usage of enums * add feeder * add feeder and adapt to new surepy version * use ProductID instead of ThingID * various changes and improvements * add connectivity sensors for all devices & proper support for multiple hubs * remove "side effects"/exception catching in attribs * correct unique ids, reorder classes * move Flap class from binary_sensor to sensor and add a sensore base class * comments cleanup, minor typing and logging fixes * remove commented code * remove commented code * add typing * 100% battery_level is enough * human-friendly datetime * better enum usage * add online and learning mode attrs * use max two decimals in attrs * use legacy style debug logging * remove str usage of enums * add feeder * add feeder and adapt to new surepy version * use ProductID instead of ThingID * various changes and improvements * add connectivity sensors for all devices & proper support for multiple hubs * remove "side effects"/exception catching in attribs * correct unique ids, reorder classes * move Flap class from binary_sensor to sensor and add a sensore base class * comments cleanup, minor typing and logging fixes * remove commented code * remove commented code * fix spelling in comment to make the CI happy (seriously?!) * fix manifest file * fix requirements_all.txt file * add missing docstrings * fix available property * remove typing from self * remove commented code * remove is_on property from sensor * jump to new surepy version * remove useles init methods --- .../components/surepetcare/__init__.py | 98 ++++---- .../components/surepetcare/binary_sensor.py | 215 ++++++++++++------ homeassistant/components/surepetcare/const.py | 3 + .../components/surepetcare/manifest.json | 2 +- .../components/surepetcare/sensor.py | 201 ++++++++++------ requirements_all.txt | 2 +- 6 files changed, 328 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 450d7eb9a15..a22ba4a1335 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -1,17 +1,17 @@ """Support for Sure Petcare cat/pet flaps.""" import logging +from typing import Any, Dict, List from surepy import ( SurePetcare, SurePetcareAuthenticationError, SurePetcareError, - SureThingID, + SureProductID, ) import voluptuous as vol from homeassistant.const import ( CONF_ID, - CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TYPE, @@ -23,9 +23,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from .const import ( + CONF_FEEDERS, CONF_FLAPS, - CONF_HOUSEHOLD_ID, + CONF_PARENT, CONF_PETS, + CONF_PRODUCT_ID, DATA_SURE_PETCARE, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -36,23 +38,19 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -FLAP_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string} -) - -PET_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string} -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_HOUSEHOLD_ID): cv.positive_int, - vol.Required(CONF_FLAPS): vol.All(cv.ensure_list, [FLAP_SCHEMA]), - vol.Required(CONF_PETS): vol.All(cv.ensure_list, [PET_SCHEMA]), + vol.Optional(CONF_FEEDERS, default=[]): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_FLAPS, default=[]): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]), vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, @@ -63,7 +61,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass, config) -> bool: """Initialize the Sure Petcare component.""" conf = config[DOMAIN] @@ -78,11 +76,10 @@ async def async_setup(hass, config): surepy = SurePetcare( conf[CONF_USERNAME], conf[CONF_PASSWORD], - conf[CONF_HOUSEHOLD_ID], hass.loop, async_get_clientsession(hass), ) - await surepy.refresh_token() + await surepy.get_data() except SurePetcareAuthenticationError: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") return False @@ -90,32 +87,44 @@ async def async_setup(hass, config): _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) return False - # add flaps + # add feeders things = [ - { - CONF_NAME: flap[CONF_NAME], - CONF_ID: flap[CONF_ID], - CONF_TYPE: SureThingID.FLAP.name, - } - for flap in conf[CONF_FLAPS] + {CONF_ID: feeder, CONF_TYPE: SureProductID.FEEDER} + for feeder in conf[CONF_FEEDERS] ] - # add pets + # add flaps (don't differentiate between CAT and PET for now) things.extend( [ - { - CONF_NAME: pet[CONF_NAME], - CONF_ID: pet[CONF_ID], - CONF_TYPE: SureThingID.PET.name, - } - for pet in conf[CONF_PETS] + {CONF_ID: flap, CONF_TYPE: SureProductID.PET_FLAP} + for flap in conf[CONF_FLAPS] ] ) - spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI( - hass, surepy, things, conf[CONF_HOUSEHOLD_ID] + # discover hubs the flaps/feeders are connected to + for device in things.copy(): + device_data = await surepy.device(device[CONF_ID]) + if ( + CONF_PARENT in device_data + and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SureProductID.HUB + and device_data[CONF_PARENT][CONF_ID] not in things + ): + things.append( + { + CONF_ID: device_data[CONF_PARENT][CONF_ID], + CONF_TYPE: SureProductID.HUB, + } + ) + + # add pets + things.extend( + [{CONF_ID: pet, CONF_TYPE: SureProductID.PET} for pet in conf[CONF_PETS]] ) + _LOGGER.debug("Devices and Pets to setup: %s", things) + + spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(hass, surepy, things) + # initial update await spc.async_update() @@ -135,16 +144,18 @@ async def async_setup(hass, config): class SurePetcareAPI: """Define a generic Sure Petcare object.""" - def __init__(self, hass, surepy, ids, household_id): + def __init__(self, hass, surepy: SurePetcare, ids: List[Dict[str, Any]]) -> None: """Initialize the Sure Petcare object.""" self.hass = hass self.surepy = surepy - self.household_id = household_id self.ids = ids - self.states = {} + self.states: Dict[str, Any] = {} - async def async_update(self, args=None): + async def async_update(self, arg: Any = None) -> None: """Refresh Sure Petcare data.""" + + await self.surepy.get_data() + for thing in self.ids: sure_id = thing[CONF_ID] sure_type = thing[CONF_TYPE] @@ -152,10 +163,15 @@ class SurePetcareAPI: try: type_state = self.states.setdefault(sure_type, {}) - if sure_type == SureThingID.FLAP.name: - type_state[sure_id] = await self.surepy.get_flap_data(sure_id) - elif sure_type == SureThingID.PET.name: - type_state[sure_id] = await self.surepy.get_pet_data(sure_id) + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + SureProductID.FEEDER, + SureProductID.HUB, + ]: + type_state[sure_id] = await self.surepy.device(sure_id) + elif sure_type == SureProductID.PET: + type_state[sure_id] = await self.surepy.pet(sure_id) except SurePetcareError as error: _LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 100da5cb790..5b3ac492137 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -1,23 +1,28 @@ """Support for Sure PetCare Flaps/Pets binary sensors.""" +from datetime import datetime import logging +from typing import Any, Dict, Optional -from surepy import SureLocationID, SureLockStateID, SureThingID +from surepy import SureLocationID, SureProductID from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_LOCK, + DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PRESENCE, BinarySensorDevice, ) -from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_ID, CONF_TYPE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_SURE_PETCARE, DEFAULT_DEVICE_CLASS, SPC, TOPIC_UPDATE +from . import SurePetcareAPI +from .const import DATA_SURE_PETCARE, SPC, TOPIC_UPDATE _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up Sure PetCare Flaps sensors based on a config entry.""" if discovery_info is None: return @@ -30,10 +35,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sure_id = thing[CONF_ID] sure_type = thing[CONF_TYPE] - if sure_type == SureThingID.FLAP.name: - entity = Flap(sure_id, thing[CONF_NAME], spc) - elif sure_type == SureThingID.PET.name: - entity = Pet(sure_id, thing[CONF_NAME], spc) + # connectivity + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + SureProductID.FEEDER, + ]: + entities.append(DeviceConnectivity(sure_id, sure_type, spc)) + + if sure_type == SureProductID.PET: + entity = Pet(sure_id, spc) + elif sure_type == SureProductID.HUB: + entity = Hub(sure_id, spc) + else: + continue entities.append(entity) @@ -44,57 +59,67 @@ class SurePetcareBinarySensor(BinarySensorDevice): """A binary sensor implementation for Sure Petcare Entities.""" def __init__( - self, _id: int, name: str, spc, device_class: str, sure_type: SureThingID + self, + _id: int, + spc: SurePetcareAPI, + device_class: str, + sure_type: SureProductID, ): """Initialize a Sure Petcare binary sensor.""" self._id = _id - self._name = name - self._spc = spc - self._device_class = device_class self._sure_type = sure_type - self._state = {} + self._device_class = device_class + + self._spc: SurePetcareAPI = spc + self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._state: Dict[str, Any] = {} + + # cover special case where a device has no name set + if "name" in self._spc_data: + name = self._spc_data["name"] + else: + name = f"Unnamed {self._sure_type.name.capitalize()}" + + self._name = f"{self._sure_type.name.capitalize()} {name.capitalize()}" self._async_unsub_dispatcher_connect = None @property - def is_on(self): + def is_on(self) -> Optional[bool]: """Return true if entity is on/unlocked.""" return bool(self._state) @property - def should_poll(self): + def should_poll(self) -> bool: """Return true.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self._name @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - return self._state - - @property - def device_class(self): + def device_class(self) -> str: """Return the device class.""" - return DEFAULT_DEVICE_CLASS if not self._device_class else self._device_class + return None if not self._device_class else self._device_class @property - def unique_id(self): + def unique_id(self: BinarySensorDevice) -> str: """Return an unique ID.""" - return f"{self._spc.household_id}-{self._id}" + return f"{self._spc_data['household_id']}-{self._id}" - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" - self._state = self._spc.states[self._sure_type][self._id].get("data") + self._spc_data = self._spc.states[self._sure_type].get(self._id) + self._state = self._spc_data.get("status") + _LOGGER.debug("%s -> self._state: %s", self._name, self._state) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" self.async_schedule_update_ha_state(True) @@ -102,54 +127,38 @@ class SurePetcareBinarySensor(BinarySensorDevice): self.hass, TOPIC_UPDATE, update ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() -class Flap(SurePetcareBinarySensor): - """Sure Petcare Flap.""" +class Hub(SurePetcareBinarySensor): + """Sure Petcare Pet.""" - def __init__(self, _id: int, name: str, spc): - """Initialize a Sure Petcare Flap.""" - super().__init__( - _id, - f"Flap {name.capitalize()}", - spc, - DEVICE_CLASS_LOCK, - SureThingID.FLAP.name, - ) + def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + """Initialize a Sure Petcare Hub.""" + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SureProductID.HUB) @property - def is_on(self): - """Return true if entity is on/unlocked.""" - try: - return bool(self._state["locking"]["mode"] == SureLockStateID.UNLOCKED) - except (KeyError, TypeError): - return None + def available(self) -> bool: + """Return true if entity is available.""" + return bool(self._state["online"]) @property - def device_state_attributes(self): + def is_on(self) -> bool: + """Return true if entity is online.""" + return self.available + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes of the device.""" attributes = None if self._state: - try: - attributes = { - "battery_voltage": self._state["battery"] / 4, - "locking_mode": self._state["locking"]["mode"], - "device_rssi": self._state["signal"]["device_rssi"], - "hub_rssi": self._state["signal"]["hub_rssi"], - } - - except (KeyError, TypeError) as error: - _LOGGER.error( - "Error getting device state attributes from %s: %s\n\n%s", - self._name, - error, - self._state, - ) - attributes = self._state + attributes = { + "led_mode": int(self._state["led_mode"]), + "pairing_mode": bool(self._state["pairing_mode"]), + } return attributes @@ -157,20 +166,76 @@ class Flap(SurePetcareBinarySensor): class Pet(SurePetcareBinarySensor): """Sure Petcare Pet.""" - def __init__(self, _id: int, name: str, spc): + def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__( - _id, - f"Pet {name.capitalize()}", - spc, - DEVICE_CLASS_PRESENCE, - SureThingID.PET.name, - ) + super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SureProductID.PET) @property - def is_on(self): + def is_on(self) -> bool: """Return true if entity is at home.""" try: - return bool(self._state["where"] == SureLocationID.INSIDE) + return bool(SureLocationID(self._state["where"]) == SureLocationID.INSIDE) except (KeyError, TypeError): return False + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the device.""" + attributes = None + if self._state: + attributes = { + "since": str( + datetime.fromisoformat(self._state["since"]).replace(tzinfo=None) + ), + "where": SureLocationID(self._state["where"]).name.capitalize(), + } + + return attributes + + async def async_update(self) -> None: + """Get the latest data and update the state.""" + self._spc_data = self._spc.states[self._sure_type].get(self._id) + self._state = self._spc_data.get("position") + _LOGGER.debug("%s -> self._state: %s", self._name, self._state) + + +class DeviceConnectivity(SurePetcareBinarySensor): + """Sure Petcare Pet.""" + + def __init__( + self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI, + ) -> None: + """Initialize a Sure Petcare Device.""" + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, sure_type) + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{self._name}_connectivity" + + @property + def unique_id(self: BinarySensorDevice) -> str: + """Return an unique ID.""" + return f"{self._spc_data['household_id']}-{self._id}-connectivity" + + @property + def available(self) -> bool: + """Return true if entity is available.""" + return bool(self._state) + + @property + def is_on(self) -> bool: + """Return true if entity is online.""" + return self.available + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the device.""" + attributes = None + if self._state: + attributes = { + "device_rssi": f'{self._state["signal"]["device_rssi"]:.2f}', + "hub_rssi": f'{self._state["signal"]["hub_rssi"]:.2f}', + } + + return attributes diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 731bfba07e6..d534398784f 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -11,8 +11,11 @@ SPC = "spc" SUREPY = "surepy" CONF_HOUSEHOLD_ID = "household_id" +CONF_FEEDERS = "feeders" CONF_FLAPS = "flaps" +CONF_PARENT = "parent" CONF_PETS = "pets" +CONF_PRODUCT_ID = "product_id" CONF_DATA = "data" SURE_IDS = "sure_ids" diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index b4879932714..b1efa4ce639 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/surepetcare", "dependencies": [], "codeowners": ["@benleb"], - "requirements": ["surepy==0.1.10"] + "requirements": ["surepy==0.2.3"] } diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index dd7fdcb0316..8dc9cf30e3c 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -1,19 +1,15 @@ """Support for Sure PetCare Flaps/Pets sensors.""" import logging +from typing import Any, Dict, Optional -from surepy import SureThingID +from surepy import SureLockStateID, SureProductID -from homeassistant.const import ( - ATTR_VOLTAGE, - CONF_ID, - CONF_NAME, - CONF_TYPE, - DEVICE_CLASS_BATTERY, -) +from homeassistant.const import ATTR_VOLTAGE, CONF_ID, CONF_TYPE, DEVICE_CLASS_BATTERY from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from . import SurePetcareAPI from .const import ( DATA_SURE_PETCARE, SPC, @@ -30,97 +26,82 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return + entities = [] + spc = hass.data[DATA_SURE_PETCARE][SPC] - async_add_entities( - [ - FlapBattery(entity[CONF_ID], entity[CONF_NAME], spc) - for entity in spc.ids - if entity[CONF_TYPE] == SureThingID.FLAP.name - ], - True, - ) + + for entity in spc.ids: + sure_type = entity[CONF_TYPE] + + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + SureProductID.FEEDER, + ]: + entities.append(SureBattery(entity[CONF_ID], sure_type, spc)) + + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + ]: + entities.append(Flap(entity[CONF_ID], sure_type, spc)) + + async_add_entities(entities, True) -class FlapBattery(Entity): - """Sure Petcare Flap.""" +class SurePetcareSensor(Entity): + """A binary sensor implementation for Sure Petcare Entities.""" + + def __init__( + self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI, + ): + """Initialize a Sure Petcare sensor.""" - def __init__(self, _id: int, name: str, spc): - """Initialize a Sure Petcare Flap battery sensor.""" self._id = _id - self._name = f"Flap {name.capitalize()} Battery Level" + self._sure_type = sure_type + self._spc = spc - self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data") + self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._state: Dict[str, Any] = {} + + self._name = ( + f"{self._sure_type.name.capitalize()} " + f"{self._spc_data['name'].capitalize()}" + ) self._async_unsub_dispatcher_connect = None @property - def should_poll(self): - """Return true.""" - return False - - @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self._name @property - def state(self): - """Return battery level in percent.""" - try: - per_battery_voltage = self._state["battery"] / 4 - voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW - battery_percent = int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100) - except (KeyError, TypeError): - battery_percent = None - - return battery_percent - - @property - def unique_id(self): + def unique_id(self) -> str: """Return an unique ID.""" - return f"{self._spc.household_id}-{self._id}" + return f"{self._spc_data['household_id']}-{self._id}" @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_BATTERY + def available(self) -> bool: + """Return true if entity is available.""" + return bool(self._state) @property - def device_state_attributes(self): - """Return state attributes.""" - attributes = None - if self._state: - try: - voltage_per_battery = float(self._state["battery"]) / 4 - attributes = { - ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", - f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", - } - except (KeyError, TypeError) as error: - attributes = self._state - _LOGGER.error( - "Error getting device state attributes from %s: %s\n\n%s", - self._name, - error, - self._state, - ) + def should_poll(self) -> bool: + """Return true.""" + return False - return attributes - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return "%" - - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" - self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data") + self._spc_data = self._spc.states[self._sure_type].get(self._id) + self._state = self._spc_data.get("status") + _LOGGER.debug("%s -> self._state: %s", self._name, self._state) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" self.async_schedule_update_ha_state(True) @@ -128,7 +109,77 @@ class FlapBattery(Entity): self.hass, TOPIC_UPDATE, update ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() + + +class Flap(SurePetcareSensor): + """Sure Petcare Flap.""" + + @property + def state(self) -> Optional[int]: + """Return battery level in percent.""" + return SureLockStateID(self._state["locking"]["mode"]).name.capitalize() + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the device.""" + attributes = None + if self._state: + attributes = { + "learn_mode": bool(self._state["learn_mode"]), + } + + return attributes + + +class SureBattery(SurePetcareSensor): + """Sure Petcare Flap.""" + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{self._name} Battery Level" + + @property + def state(self) -> Optional[int]: + """Return battery level in percent.""" + battery_percent: Optional[int] + try: + per_battery_voltage = self._state["battery"] / 4 + voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW + battery_percent = min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100) + except (KeyError, TypeError): + battery_percent = None + + return battery_percent + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return f"{self._spc_data['household_id']}-{self._id}-battery" + + @property + def device_class(self) -> str: + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return state attributes.""" + attributes = None + if self._state: + voltage_per_battery = float(self._state["battery"]) / 4 + attributes = { + ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", + f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", + } + + return attributes + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + return "%" diff --git a/requirements_all.txt b/requirements_all.txt index 159e041b731..044c321bd95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1928,7 +1928,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.1.10 +surepy==0.2.3 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 From 41f3fb291afe6596f276400e8c692e815172f40d Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Sun, 9 Feb 2020 18:13:22 +0100 Subject: [PATCH 174/378] Add ZHA Texas Instruments CC device support (#31621) --- homeassistant/components/zha/core/const.py | 9 ++++++--- homeassistant/components/zha/core/gateway.py | 11 +++++++---- homeassistant/components/zha/core/registries.py | 15 +++++++++++---- homeassistant/components/zha/manifest.json | 1 + requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index b8782101cd4..f4cccfa4e52 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -96,6 +96,7 @@ DATA_ZHA_GATEWAY = "zha_gateway" DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" DEBUG_COMP_ZIGPY = "zigpy" +DEBUG_COMP_ZIGPY_CC = "zigpy_cc" DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" @@ -105,8 +106,9 @@ DEBUG_LEVELS = { DEBUG_COMP_BELLOWS: logging.DEBUG, DEBUG_COMP_ZHA: logging.DEBUG, DEBUG_COMP_ZIGPY: logging.DEBUG, - DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, + DEBUG_COMP_ZIGPY_CC: logging.DEBUG, DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, + DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, } DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] @@ -131,9 +133,10 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" class RadioType(enum.Enum): """Possible options for radio type.""" - ezsp = "ezsp" - xbee = "xbee" deconz = "deconz" + ezsp = "ezsp" + ti_cc = "ti_cc" + xbee = "xbee" zigate = "zigate" @classmethod diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index dd661835367..33faaa334cb 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -41,6 +41,7 @@ from .const import ( DEBUG_COMP_BELLOWS, DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY, + DEBUG_COMP_ZIGPY_CC, DEBUG_COMP_ZIGPY_DECONZ, DEBUG_COMP_ZIGPY_XBEE, DEBUG_COMP_ZIGPY_ZIGATE, @@ -555,12 +556,13 @@ def async_capture_log_levels(): DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(), DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(), - DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( - DEBUG_COMP_ZIGPY_XBEE - ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_CC: logging.getLogger(DEBUG_COMP_ZIGPY_CC).getEffectiveLevel(), DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( DEBUG_COMP_ZIGPY_DECONZ ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( + DEBUG_COMP_ZIGPY_XBEE + ).getEffectiveLevel(), DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger( DEBUG_COMP_ZIGPY_ZIGATE ).getEffectiveLevel(), @@ -573,8 +575,9 @@ def async_set_logger_levels(levels): logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) - logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) + logging.getLogger(DEBUG_COMP_ZIGPY_CC).setLevel(levels[DEBUG_COMP_ZIGPY_CC]) logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE]) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 311f8fa275f..4f5c6fc5c6b 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -13,6 +13,8 @@ import bellows.zigbee.application import zigpy.profiles.zha import zigpy.profiles.zll import zigpy.zcl as zcl +import zigpy_cc.api +import zigpy_cc.zigbee.application import zigpy_deconz.api import zigpy_deconz.zigbee.application import zigpy_xbee.api @@ -127,15 +129,20 @@ LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() RADIO_TYPES = { + RadioType.deconz.name: { + ZHA_GW_RADIO: zigpy_deconz.api.Deconz, + CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "Deconz", + }, RadioType.ezsp.name: { ZHA_GW_RADIO: bellows.ezsp.EZSP, CONTROLLER: bellows.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "EZSP", }, - RadioType.deconz.name: { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz, - CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "Deconz", + RadioType.ti_cc.name: { + ZHA_GW_RADIO: zigpy_cc.api.API, + CONTROLLER: zigpy_cc.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "TI CC", }, RadioType.xbee.name: { ZHA_GW_RADIO: zigpy_xbee.api.XBee, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f5ec96690bc..c0b7a0719f8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,6 +6,7 @@ "requirements": [ "bellows-homeassistant==0.13.2", "zha-quirks==0.0.32", + "zigpy-cc==0.1.0", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.13.2", "zigpy-xbee-homeassistant==0.9.0", diff --git a/requirements_all.txt b/requirements_all.txt index 044c321bd95..3a46e3514e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2127,6 +2127,9 @@ zhong_hong_hvac==1.0.9 # homeassistant.components.ziggo_mediabox_xl ziggo-mediabox-xl==1.1.0 +# homeassistant.components.zha +zigpy-cc==0.1.0 + # homeassistant.components.zha zigpy-deconz==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c82ade3495..e4fa6515bab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -714,6 +714,9 @@ zeroconf==0.24.4 # homeassistant.components.zha zha-quirks==0.0.32 +# homeassistant.components.zha +zigpy-cc==0.1.0 + # homeassistant.components.zha zigpy-deconz==0.7.0 From 118ba10442306b3d5dad939b289f9c68a0290467 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 10 Feb 2020 00:31:37 +0000 Subject: [PATCH 175/378] [ci skip] Translation update --- .../homekit_controller/.translations/de.json | 2 +- .../components/mikrotik/.translations/de.json | 1 + .../minecraft_server/.translations/ca.json | 24 +++++++++++++++++++ .../minecraft_server/.translations/de.json | 24 +++++++++++++++++++ .../minecraft_server/.translations/no.json | 3 +++ .../minecraft_server/.translations/tr.json | 24 +++++++++++++++++++ .../components/mqtt/.translations/de.json | 2 +- .../components/openuv/.translations/de.json | 2 +- .../components/ps4/.translations/de.json | 2 +- .../samsungtv/.translations/de.json | 4 ++-- .../samsungtv/.translations/tr.json | 24 +++++++++++++++++++ .../components/sentry/.translations/de.json | 2 +- .../simplisafe/.translations/de.json | 2 +- .../components/spotify/.translations/tr.json | 18 ++++++++++++++ .../components/tradfri/.translations/de.json | 2 +- .../components/withings/.translations/de.json | 2 +- 16 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/minecraft_server/.translations/ca.json create mode 100644 homeassistant/components/minecraft_server/.translations/de.json create mode 100644 homeassistant/components/minecraft_server/.translations/tr.json create mode 100644 homeassistant/components/samsungtv/.translations/tr.json create mode 100644 homeassistant/components/spotify/.translations/tr.json diff --git a/homeassistant/components/homekit_controller/.translations/de.json b/homeassistant/components/homekit_controller/.translations/de.json index e6942a125cd..8223616f11e 100644 --- a/homeassistant/components/homekit_controller/.translations/de.json +++ b/homeassistant/components/homekit_controller/.translations/de.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Kopplungscode" }, - "description": "Gebe deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", + "description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "user": { diff --git a/homeassistant/components/mikrotik/.translations/de.json b/homeassistant/components/mikrotik/.translations/de.json index d3328e9c305..97d28db4cfb 100644 --- a/homeassistant/components/mikrotik/.translations/de.json +++ b/homeassistant/components/mikrotik/.translations/de.json @@ -27,6 +27,7 @@ "step": { "device_tracker": { "data": { + "arp_ping": "ARP Ping aktivieren", "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" } } diff --git a/homeassistant/components/minecraft_server/.translations/ca.json b/homeassistant/components/minecraft_server/.translations/ca.json new file mode 100644 index 00000000000..86856ac2d11 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3 amb el servidor. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que estas utilitzant la versi\u00f3 del servidor 1.7 o superior.", + "invalid_ip": "L\u2019adre\u00e7a IP \u00e9s inv\u00e0lida (no s\u2019ha pogut determinar l\u2019adre\u00e7a MAC). Corregeix-la i torna-ho a provar.", + "invalid_port": "El port ha d'estar compr\u00e8s entre 1024 i 65535. Corregeix-lo i torna-ho a provar." + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "port": "Port" + }, + "description": "Configuraci\u00f3 d'una inst\u00e0ncia de servidor de Minecraft per poder monitoritzar-lo.", + "title": "Enlla\u00e7 del servidor de Minecraft" + } + }, + "title": "Servidor de Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/de.json b/homeassistant/components/minecraft_server/.translations/de.json new file mode 100644 index 00000000000..00426308239 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Der Host ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung zum Server fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Stelle au\u00dferdem sicher, dass Du mindestens Minecraft Version 1.7 auf Deinem Server ausf\u00fchrst.", + "invalid_ip": "IP-Adresse ist ung\u00fcltig (MAC-Adresse konnte nicht ermittelt werden). Bitte korrigieren und erneut versuchen.", + "invalid_port": "Der Port muss im Bereich von 1024 bis 65535 liegen. Bitte korrigieren und erneut versuchen." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + }, + "description": "Richte deine Minecraft Server-Instanz ein, um es \u00fcberwachen zu k\u00f6nnen.", + "title": "Verkn\u00fcpfe deinen Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json index aebd8c59136..fad919c0473 100644 --- a/homeassistant/components/minecraft_server/.translations/no.json +++ b/homeassistant/components/minecraft_server/.translations/no.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Verten er allerede konfigurert." }, + "error": { + "invalid_port": "Porten m\u00e5 v\u00e6re i omr\u00e5det 1024 til 65535. Vennligst korriger den og pr\u00f8v p\u00e5 nytt." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/minecraft_server/.translations/tr.json b/homeassistant/components/minecraft_server/.translations/tr.json new file mode 100644 index 00000000000..595c1686982 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Host zaten ayarlanm\u0131\u015f." + }, + "error": { + "cannot_connect": "Server ile ba\u011flant\u0131 kurulamad\u0131. L\u00fctfen host ve port ayarlar\u0131n\u0131 kontrol et ve tekrar dene. Ayr\u0131ca, serverda en az Minecraft s\u00fcr\u00fcm 1.7 \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131ndan emin ol.", + "invalid_ip": "IP adresi ge\u00e7ersiz (MAC adresi belirlenemedi). L\u00fctfen d\u00fczelt ve tekrar dene.", + "invalid_port": "Port 1024 ile 65535 aral\u0131\u011f\u0131nda olmal\u0131d\u0131r. L\u00fctfen d\u00fczelt ve yeniden dene." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Ad", + "port": "Port" + }, + "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", + "title": "Minecraft Servern\u0131 ba\u011fla" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index d95c43cc618..7bca8de54eb 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -22,7 +22,7 @@ "data": { "discovery": "Suche aktivieren" }, - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?", "title": "MQTT Broker per Hass.io add-on" } }, diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json index 7f8121dd96b..cc8ee92df4b 100644 --- a/homeassistant/components/openuv/.translations/de.json +++ b/homeassistant/components/openuv/.translations/de.json @@ -12,7 +12,7 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, - "title": "Gebe deine Informationen ein" + "title": "Gib deine Informationen ein" } }, "title": "OpenUV" diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json index 6f4962a305d..66eaecbb548 100644 --- a/homeassistant/components/ps4/.translations/de.json +++ b/homeassistant/components/ps4/.translations/de.json @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Geben deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", + "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json index 8760347e902..27b9ecc37df 100644 --- a/homeassistant/components/samsungtv/.translations/de.json +++ b/homeassistant/components/samsungtv/.translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Dieser Samsung TV ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", - "auth_missing": "Home Assistant ist nicht authentifiziert, um eine Verbindung zu diesem Samsung TV herzustellen.", + "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", "not_found": "Keine unterst\u00fctzten Samsung TV-Ger\u00e4te im Netzwerk gefunden.", "not_successful": "Es kann keine Verbindung zu diesem Samsung-Fernsehger\u00e4t hergestellt werden.", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." @@ -19,7 +19,7 @@ "host": "Host oder IP-Adresse", "name": "Name" }, - "description": "Gebe deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.", + "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/samsungtv/.translations/tr.json b/homeassistant/components/samsungtv/.translations/tr.json new file mode 100644 index 00000000000..3cf1f135e1f --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.", + "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", + "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", + "not_found": "A\u011fda desteklenen Samsung TV cihaz\u0131 bulunamad\u0131.", + "not_successful": "Bu Samsung TV cihaz\u0131na ba\u011flan\u0131lam\u0131yor.", + "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." + }, + "flow_title": "Samsung TV: {model}", + "step": { + "user": { + "data": { + "host": "Host veya IP adresi", + "name": "Ad" + }, + "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/de.json b/homeassistant/components/sentry/.translations/de.json index ea1e3f674ae..db71d8818bc 100644 --- a/homeassistant/components/sentry/.translations/de.json +++ b/homeassistant/components/sentry/.translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Gebe deine Sentry-DSN ein", + "description": "Gib deine Sentry-DSN ein", "title": "Sentry" } }, diff --git a/homeassistant/components/simplisafe/.translations/de.json b/homeassistant/components/simplisafe/.translations/de.json index ee7eaecc852..5ebc17f13b9 100644 --- a/homeassistant/components/simplisafe/.translations/de.json +++ b/homeassistant/components/simplisafe/.translations/de.json @@ -11,7 +11,7 @@ "password": "Passwort", "username": "E-Mail-Adresse" }, - "title": "Gebe deine Informationen ein" + "title": "Gib deine Informationen ein" } }, "title": "SimpliSafe" diff --git a/homeassistant/components/spotify/.translations/tr.json b/homeassistant/components/spotify/.translations/tr.json new file mode 100644 index 00000000000..88755b800f4 --- /dev/null +++ b/homeassistant/components/spotify/.translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Yaln\u0131zca bir Spotify hesab\u0131 ayarlayabilirsin.", + "authorize_url_timeout": "Kimlik do\u011frulama URL'sini olu\u015ftururken zaman a\u015f\u0131m\u0131 ger\u00e7ekle\u015fti.", + "missing_configuration": "Spotify entegrasyonu ayarlanmam\u0131\u015f. L\u00fctfen dok\u00fcmentasyonu takip et." + }, + "create_entry": { + "default": "Spotify ile kimlik ba\u015far\u0131yla do\u011fruland\u0131." + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json index 5dc2630556e..68165dbb291 100644 --- a/homeassistant/components/tradfri/.translations/de.json +++ b/homeassistant/components/tradfri/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Bridge ist bereits konfiguriert", + "already_configured": "Bridge ist bereits konfiguriert.", "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt." }, "error": { diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index 067fa97ebdc..ae8ab679593 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -6,7 +6,7 @@ "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation." }, "create_entry": { - "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." + "default": "Erfolgreiche Authentifizierung mit Withings." }, "step": { "pick_implementation": { From 28eeed1db389c3dda138663f54401a9ce8b8bd64 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 9 Feb 2020 21:45:35 -0500 Subject: [PATCH 176/378] ZHA tests refactoring (#31682) * Fixtures for restoring/joning a device. * binary_sensor.zha tests. * cover.zha platform tests. * device_tracker.zha platform tests. * fan.zha platform tests. * switch.zha platform tests. * Update light.zha platform tests. * Update sensor.zha platform tests. * ZHA api tests refactoring. * Update lock.zha platform tests. * Update ZHA gateway tests. * Update zha device action tests. * Update zha device trigger tests. * Cleanup. --- tests/components/zha/common.py | 94 +------ tests/components/zha/conftest.py | 93 ++++--- tests/components/zha/test_api.py | 114 +++++---- tests/components/zha/test_binary_sensor.py | 128 +++++----- tests/components/zha/test_cover.py | 54 ++-- tests/components/zha/test_device_action.py | 70 ++--- tests/components/zha/test_device_tracker.py | 69 ++--- tests/components/zha/test_device_trigger.py | 103 +++----- tests/components/zha/test_fan.py | 35 +-- tests/components/zha/test_gateway.py | 46 ++-- tests/components/zha/test_light.py | 237 ++++++++--------- tests/components/zha/test_lock.py | 36 +-- tests/components/zha/test_sensor.py | 268 +++++++++----------- tests/components/zha/test_switch.py | 42 +-- 14 files changed, 648 insertions(+), 741 deletions(-) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 9b6a8b5b55f..97668bef2ea 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,6 +1,6 @@ """Common test objects.""" import time -from unittest.mock import Mock, patch +from unittest.mock import Mock from asynctest import CoroutineMock import zigpy.profiles.zha @@ -18,8 +18,6 @@ from homeassistant.components.zha.core.const import ( ) from homeassistant.util import slugify -from tests.common import mock_coro - class FakeApplication: """Fake application for mocking zigpy.""" @@ -100,66 +98,6 @@ class FakeDevice: self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0] -def make_device(endpoints, ieee, manufacturer, model): - """Make a fake device using the specified cluster classes.""" - device = FakeDevice(ieee, manufacturer, model) - for epid, ep in endpoints.items(): - endpoint = FakeEndpoint(manufacturer, model, epid) - endpoint.device = device - device.endpoints[epid] = endpoint - endpoint.device_type = ep["device_type"] - profile_id = ep.get("profile_id") - if profile_id: - endpoint.profile_id = profile_id - - for cluster_id in ep.get("in_clusters", []): - endpoint.add_input_cluster(cluster_id) - - for cluster_id in ep.get("out_clusters", []): - endpoint.add_output_cluster(cluster_id) - - return device - - -async def async_init_zigpy_device( - hass, - in_cluster_ids, - out_cluster_ids, - device_type, - gateway, - ieee="00:0d:6f:00:0a:90:69:e7", - manufacturer="FakeManufacturer", - model="FakeModel", - is_new_join=False, -): - """Create and initialize a device. - - This creates a fake device and adds it to the "network". It can be used to - test existing device functionality and new device pairing functionality. - The is_new_join parameter influences whether or not the device will go - through cluster binding and zigbee cluster configure reporting. That only - happens when the device is paired to the network for the first time. - """ - device = make_device( - { - 1: { - "in_clusters": in_cluster_ids, - "out_clusters": out_cluster_ids, - "device_type": device_type, - } - }, - ieee, - manufacturer, - model, - ) - if is_new_join: - await gateway.async_device_initialized(device) - else: - await gateway.async_device_restored(device) - await hass.async_block_till_done() - return device - - def make_attribute(attrid, value, status=0): """Make an attribute.""" attr = zcl_f.Attribute() @@ -202,36 +140,6 @@ async def async_enable_traffic(hass, zha_gateway, zha_devices): await hass.async_block_till_done() -async def async_test_device_join( - hass, zha_gateway, cluster_id, entity_id, device_type=None -): - """Test a newly joining device. - - This creates a new fake device and adds it to the network. It is meant to - simulate pairing a new device to the network so that code pathways that - only trigger during device joins can be tested. - """ - # create zigpy device mocking out the zigbee network operations - with patch( - "zigpy.zcl.Cluster.configure_reporting", - return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), - ): - with patch( - "zigpy.zcl.Cluster.bind", - return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), - ): - await async_init_zigpy_device( - hass, - [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id], - [], - device_type, - zha_gateway, - ieee="00:0d:6f:00:0a:90:69:f7", - is_new_join=True, - ) - assert hass.states.get(entity_id) is not None - - def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHeader: """Cluster.handle_message() ZCL Header helper.""" if global_command: diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 18344172d29..f54c6ca8602 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,4 +1,5 @@ """Test configuration for the ZHA component.""" +import functools from unittest import mock from unittest.mock import patch @@ -7,7 +8,6 @@ import pytest import zigpy from zigpy.application import ControllerApplication -from homeassistant import config_entries from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.store import async_get_registry @@ -15,27 +15,39 @@ from homeassistant.helpers.device_registry import async_get_registry as get_dev_ from .common import FakeDevice, FakeEndpoint, async_setup_entry +from tests.common import MockConfigEntry + FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" @pytest.fixture(name="config_entry") -def config_entry_fixture(hass): +async def config_entry_fixture(hass): """Fixture representing a config entry.""" - config_entry = config_entries.ConfigEntry( - 1, - DOMAIN, - "Mock Title", - {}, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - ) + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) return config_entry +@pytest.fixture +async def setup_zha(hass, config_entry): + """Load the ZHA component. + + This will init the ZHA component. It loads the component in HA so that + we can test the domains that ZHA supports without actually having a zigbee + network running. + """ + # this prevents needing an actual radio and zigbee network available + with patch("homeassistant.components.zha.async_setup_entry", async_setup_entry): + hass.data[DATA_ZHA] = {} + + # init ZHA + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + + @pytest.fixture(name="zha_gateway") -async def zha_gateway_fixture(hass, config_entry): +async def zha_gateway_fixture(hass, config_entry, setup_zha): """Fixture representing a zha gateway. Create a ZHAGateway object that can be used to interact with as if we @@ -57,23 +69,6 @@ async def zha_gateway_fixture(hass, config_entry): return gateway -@pytest.fixture(autouse=True) -async def setup_zha(hass, config_entry): - """Load the ZHA component. - - This will init the ZHA component. It loads the component in HA so that - we can test the domains that ZHA supports without actually having a zigbee - network running. - """ - # this prevents needing an actual radio and zigbee network available - with patch("homeassistant.components.zha.async_setup_entry", async_setup_entry): - hass.data[DATA_ZHA] = {} - - # init ZHA - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - - @pytest.fixture def channel(): """Channel mock factory fixture.""" @@ -121,3 +116,43 @@ def zigpy_device_mock(): return device return _mock_dev + + +@pytest.fixture +def _zha_device_restored_or_joined(hass, zha_gateway, config_entry): + """Make a restored or joined ZHA devices.""" + + async def _zha_device(is_new_join, zigpy_dev): + if is_new_join: + for cmp in COMPONENTS: + await hass.config_entries.async_forward_entry_setup(config_entry, cmp) + await hass.async_block_till_done() + await zha_gateway.async_device_initialized(zigpy_dev) + else: + await zha_gateway.async_device_restored(zigpy_dev) + for cmp in COMPONENTS: + await hass.config_entries.async_forward_entry_setup(config_entry, cmp) + await hass.async_block_till_done() + return zha_gateway.get_device(zigpy_dev.ieee) + + return _zha_device + + +@pytest.fixture +def zha_device_joined(_zha_device_restored_or_joined): + """Return a newly joined ZHA device.""" + + return functools.partial(_zha_device_restored_or_joined, True) + + +@pytest.fixture +def zha_device_restored(_zha_device_restored_or_joined): + """Return a restored ZHA device.""" + + return functools.partial(_zha_device_restored_or_joined, False) + + +@pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) +def zha_device_joined_restored(request): + """Join or restore ZHA device.""" + return request.getfixturevalue(request.param) diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index f01d27eb167..c61ecfa8c71 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,11 +1,9 @@ """Test ZHA API.""" import pytest -import zigpy +import zigpy.profiles.zha import zigpy.zcl.clusters.general as general -from homeassistant.components.light import DOMAIN as light_domain -from homeassistant.components.switch import DOMAIN from homeassistant.components.websocket_api import const from homeassistant.components.zha.api import ID, TYPE, async_load_api from homeassistant.components.zha.core.const import ( @@ -23,50 +21,67 @@ from homeassistant.components.zha.core.const import ( GROUP_NAME, ) -from .common import async_init_zigpy_device from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME +IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" +IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" + @pytest.fixture -async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): +async def device_switch(hass, zha_gateway, zigpy_device_mock, zha_device_joined): + """Test zha switch platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [general.OnOff.cluster_id, general.Basic.cluster_id], + "out_clusters": [], + "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, + } + }, + ieee=IEEE_SWITCH_DEVICE, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_groupable(hass, zha_gateway, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + general.Basic.cluster_id, + general.Groups.cluster_id, + ], + "out_clusters": [], + "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, + } + }, + ieee=IEEE_GROUPABLE_DEVICE, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def zha_client(hass, hass_ws_client, device_switch, device_groupable): """Test zha switch platform.""" # load the ZHA API async_load_api(hass) - - # create zigpy device - await async_init_zigpy_device( - hass, - [general.OnOff.cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, - ) - - await async_init_zigpy_device( - hass, - [general.OnOff.cluster_id, general.Basic.cluster_id, general.Groups.cluster_id], - [], - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, - zha_gateway, - manufacturer="FakeGroupManufacturer", - model="FakeGroupModel", - ieee="01:2d:6f:00:0a:90:69:e8", - ) - - # load up switch domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - await hass.config_entries.async_forward_entry_setup(config_entry, light_domain) - await hass.async_block_till_done() - return await hass_ws_client(hass) async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): """Test getting device cluster info.""" await zha_client.send_json( - {ID: 5, TYPE: "zha/devices/clusters", ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7"} + {ID: 5, TYPE: "zha/devices/clusters", ATTR_IEEE: IEEE_SWITCH_DEVICE} ) msg = await zha_client.receive_json() @@ -86,14 +101,14 @@ async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): assert cluster_info[ATTR_NAME] == "OnOff" -async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_client): +async def test_device_cluster_attributes(zha_client): """Test getting device cluster attributes.""" await zha_client.send_json( { ID: 5, TYPE: "zha/devices/clusters/attributes", ATTR_ENDPOINT_ID: 1, - ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7", + ATTR_IEEE: IEEE_SWITCH_DEVICE, ATTR_CLUSTER_ID: 6, ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN, } @@ -109,14 +124,14 @@ async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_cl assert attribute[ATTR_NAME] is not None -async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_client): +async def test_device_cluster_commands(zha_client): """Test getting device cluster commands.""" await zha_client.send_json( { ID: 5, TYPE: "zha/devices/clusters/commands", ATTR_ENDPOINT_ID: 1, - ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7", + ATTR_IEEE: IEEE_SWITCH_DEVICE, ATTR_CLUSTER_ID: 6, ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN, } @@ -133,7 +148,7 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie assert command[TYPE] is not None -async def test_list_devices(hass, config_entry, zha_gateway, zha_client): +async def test_list_devices(zha_client): """Test getting zha devices.""" await zha_client.send_json({ID: 5, TYPE: "zha/devices"}) @@ -164,7 +179,7 @@ async def test_list_devices(hass, config_entry, zha_gateway, zha_client): assert device == device2 -async def test_device_not_found(hass, config_entry, zha_gateway, zha_client): +async def test_device_not_found(zha_client): """Test not found response from get device API.""" await zha_client.send_json( {ID: 6, TYPE: "zha/device", ATTR_IEEE: "28:6d:97:00:01:04:11:8c"} @@ -176,7 +191,7 @@ async def test_device_not_found(hass, config_entry, zha_gateway, zha_client): assert msg["error"]["code"] == const.ERR_NOT_FOUND -async def test_list_groups(hass, config_entry, zha_gateway, zha_client): +async def test_list_groups(zha_client): """Test getting zha zigbee groups.""" await zha_client.send_json({ID: 7, TYPE: "zha/groups"}) @@ -193,7 +208,7 @@ async def test_list_groups(hass, config_entry, zha_gateway, zha_client): assert group["members"] == [] -async def test_get_group(hass, config_entry, zha_gateway, zha_client): +async def test_get_group(zha_client): """Test getting a specific zha zigbee group.""" await zha_client.send_json({ID: 8, TYPE: "zha/group", GROUP_ID: FIXTURE_GRP_ID}) @@ -208,7 +223,7 @@ async def test_get_group(hass, config_entry, zha_gateway, zha_client): assert group["members"] == [] -async def test_get_group_not_found(hass, config_entry, zha_gateway, zha_client): +async def test_get_group_not_found(zha_client): """Test not found response from get group API.""" await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1234567}) @@ -220,14 +235,9 @@ async def test_get_group_not_found(hass, config_entry, zha_gateway, zha_client): assert msg["error"]["code"] == const.ERR_NOT_FOUND -async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_client): +async def test_list_groupable_devices(zha_client, device_groupable): """Test getting zha devices that have a group cluster.""" - # Make device available - zha_gateway.devices[ - zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8") - ].set_available(True) - await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) msg = await zha_client.receive_json() @@ -251,9 +261,7 @@ async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_clien # Make sure there are no groupable devices when the device is unavailable # Make device unavailable - zha_gateway.devices[ - zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8") - ].set_available(False) + device_groupable.set_available(False) await zha_client.send_json({ID: 11, TYPE: "zha/devices/groupable"}) @@ -265,7 +273,7 @@ async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_clien assert len(devices) == 0 -async def test_add_group(hass, config_entry, zha_gateway, zha_client): +async def test_add_group(zha_client): """Test adding and getting a new zha zigbee group.""" await zha_client.send_json({ID: 12, TYPE: "zha/group/add", GROUP_NAME: "new_group"}) @@ -291,7 +299,7 @@ async def test_add_group(hass, config_entry, zha_gateway, zha_client): assert group["name"] == FIXTURE_GRP_NAME or group["name"] == "new_group" -async def test_remove_group(hass, config_entry, zha_gateway, zha_client): +async def test_remove_group(zha_client): """Test removing a new zha zigbee group.""" await zha_client.send_json({ID: 14, TYPE: "zha/groups"}) diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 2765a465ace..4be5f3c1f39 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,5 +1,5 @@ """Test zha binary sensor.""" -import zigpy.zcl.clusters.general as general +import pytest import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f @@ -9,76 +9,27 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, ) +DEVICE_IAS = { + 1: { + "device_type": 1026, + "in_clusters": [security.IasZone.cluster_id], + "out_clusters": [], + } +} -async def test_binary_sensor(hass, config_entry, zha_gateway): - """Test zha binary_sensor platform.""" - # create zigpy devices - zigpy_device_zone = await async_init_zigpy_device( - hass, - [security.IasZone.cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, - ieee="00:0d:6f:11:9a:90:69:e6", - ) - - zigpy_device_occupancy = await async_init_zigpy_device( - hass, - [measurement.OccupancySensing.cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, - ieee="00:0d:6f:11:9a:90:69:e7", - manufacturer="FakeOccupancy", - model="FakeOccupancyModel", - ) - - # load up binary_sensor domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - - # on off binary_sensor - zone_cluster = zigpy_device_zone.endpoints.get(1).ias_zone - zone_zha_device = zha_gateway.get_device(zigpy_device_zone.ieee) - zone_entity_id = await find_entity_id(DOMAIN, zone_zha_device, hass) - assert zone_entity_id is not None - - # occupancy binary_sensor - occupancy_cluster = zigpy_device_occupancy.endpoints.get(1).occupancy - occupancy_zha_device = zha_gateway.get_device(zigpy_device_occupancy.ieee) - occupancy_entity_id = await find_entity_id(DOMAIN, occupancy_zha_device, hass) - assert occupancy_entity_id is not None - - # test that the sensors exist and are in the unavailable state - assert hass.states.get(zone_entity_id).state == STATE_UNAVAILABLE - assert hass.states.get(occupancy_entity_id).state == STATE_UNAVAILABLE - - await async_enable_traffic( - hass, zha_gateway, [zone_zha_device, occupancy_zha_device] - ) - - # test that the sensors exist and are in the off state - assert hass.states.get(zone_entity_id).state == STATE_OFF - assert hass.states.get(occupancy_entity_id).state == STATE_OFF - - # test getting messages that trigger and reset the sensors - await async_test_binary_sensor_on_off(hass, occupancy_cluster, occupancy_entity_id) - - # test IASZone binary sensors - await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id) - - # test new sensor join - await async_test_device_join( - hass, zha_gateway, measurement.OccupancySensing.cluster_id, occupancy_entity_id - ) +DEVICE_OCCUPANCY = { + 1: { + "device_type": 263, + "in_clusters": [measurement.OccupancySensing.cluster_id], + "out_clusters": [], + } +} async def async_test_binary_sensor_on_off(hass, cluster, entity_id): @@ -109,3 +60,52 @@ async def async_test_iaszone_on_off(hass, cluster, entity_id): cluster.listener_event("cluster_command", 1, 0, [0]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF + + +@pytest.mark.parametrize( + "device, on_off_test, cluster_name, reporting", + [ + (DEVICE_IAS, async_test_iaszone_on_off, "ias_zone", False), + (DEVICE_OCCUPANCY, async_test_binary_sensor_on_off, "occupancy", True), + ], +) +async def test_binary_sensor( + hass, + zha_gateway, + zigpy_device_mock, + zha_device_joined_restored, + device, + on_off_test, + cluster_name, + reporting, +): + """Test ZHA binary_sensor platform.""" + zigpy_device = zigpy_device_mock(device) + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + + assert entity_id is not None + + # test that the sensors exist and are in the unavailable state + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the sensors exist and are in the off state + assert hass.states.get(entity_id).state == STATE_OFF + + # test getting messages that trigger and reset the sensors + cluster = getattr(zigpy_device.endpoints[1], cluster_name) + await on_off_test(hass, cluster, entity_id) + + # test rejoin + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + if reporting: + assert cluster.configure_reporting.call_count > 0 + assert cluster.configure_reporting.await_count > 0 diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 9d1c019c718..321ad95e73e 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,9 +1,10 @@ """Test zha cover.""" from unittest.mock import MagicMock, call, patch +import asynctest +import pytest import zigpy.types import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f from homeassistant.components.cover import DOMAIN @@ -11,8 +12,6 @@ from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, @@ -21,17 +20,27 @@ from .common import ( from tests.common import mock_coro -async def test_cover(hass, config_entry, zha_gateway): - """Test zha cover platform.""" +@pytest.fixture +def zigpy_cover_device(zigpy_device_mock): + """Zigpy cover device.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, - [closures.WindowCovering.cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, - ) + endpoints = { + 1: { + "device_type": 1026, + "in_clusters": [closures.WindowCovering.cluster_id], + "out_clusters": [], + } + } + return zigpy_device_mock(endpoints) + + +@asynctest.patch( + "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" +) +async def test_cover( + m1, hass, zha_gateway, zha_device_joined_restored, zigpy_cover_device +): + """Test zha cover platform.""" async def get_chan_attr(*args, **kwargs): return 100 @@ -41,13 +50,11 @@ async def test_cover(hass, config_entry, zha_gateway): new=MagicMock(side_effect=get_chan_attr), ) as get_attr_mock: # load up cover domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() + 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" - cluster = zigpy_device.endpoints.get(1).window_covering - zha_device = zha_gateway.get_device(zigpy_device.ieee) + cluster = zigpy_cover_device.endpoints.get(1).window_covering entity_id = await find_entity_id(DOMAIN, zha_device, hass) assert entity_id is not None @@ -124,6 +131,13 @@ async def test_cover(hass, config_entry, zha_gateway): False, 0x2, (), expect_reply=True, manufacturer=None ) - await async_test_device_join( - hass, zha_gateway, closures.WindowCovering.cluster_id, entity_id - ) + # test rejoin + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_cover_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OPEN + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == 1 + assert cluster.configure_reporting.await_count == 1 diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index c3195559d20..4f41878f952 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -11,12 +11,10 @@ from homeassistant.components.device_automation import ( _async_get_device_automations as async_get_device_automations, ) from homeassistant.components.zha import DOMAIN -from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.components.zha.core.const import CHANNEL_EVENT_RELAY from homeassistant.helpers.device_registry import async_get_registry from homeassistant.setup import async_setup_component -from .common import async_enable_traffic, async_init_zigpy_device - from tests.common import async_mock_service, mock_coro SHORT_PRESS = "remote_button_short_press" @@ -30,28 +28,31 @@ def calls(hass): return async_mock_service(hass, "zha", "warning_device_warn") -async def test_get_actions(hass, config_entry, zha_gateway): - """Test we get the expected actions from a zha device.""" +@pytest.fixture +async def device_ias(hass, zha_gateway, zigpy_device_mock, zha_device_joined_restored): + """IAS device fixture.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, - [ - general.Basic.cluster_id, - security.IasZone.cluster_id, - security.IasWd.cluster_id, - ], - [], - None, - zha_gateway, + clusters = [general.Basic, security.IasZone, security.IasWd] + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [c.cluster_id for c in clusters], + "out_clusters": [general.OnOff.cluster_id], + "device_type": 0, + } + }, ) - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") + zha_device = await zha_device_joined_restored(zigpy_device) + zha_device.update_available(True) await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) + return zigpy_device, zha_device - zha_device = zha_gateway.get_device(zigpy_device.ieee) - ieee_address = str(zha_device.ieee) + +async def test_get_actions(hass, device_ias): + """Test we get the expected actions from a zha device.""" + + ieee_address = str(device_ias[0].ieee) ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) @@ -66,40 +67,19 @@ async def test_get_actions(hass, config_entry, zha_gateway): assert actions == expected_actions -async def test_action(hass, config_entry, zha_gateway, calls): +async def test_action(hass, calls, device_ias): """Test for executing a zha device action.""" - - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, - [ - general.Basic.cluster_id, - security.IasZone.cluster_id, - security.IasWd.cluster_id, - ], - [general.OnOff.cluster_id], - None, - zha_gateway, - ) + zigpy_device, zha_device = device_ias zigpy_device.device_automation_triggers = { (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE} } - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - zha_device = zha_gateway.get_device(zigpy_device.ieee) ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) - with patch( "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), @@ -129,8 +109,8 @@ async def test_action(hass, config_entry, zha_gateway, calls): await hass.async_block_till_done() - on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] - on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + channel = {ch.name: ch for ch in zha_device.all_channels}[CHANNEL_EVENT_RELAY] + channel.zha_send_event(channel.cluster, COMMAND_SINGLE, []) await hass.async_block_till_done() assert len(calls) == 1 diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index bac338ae5e0..fe5661c4776 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -2,6 +2,7 @@ from datetime import timedelta import time +import pytest import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f @@ -14,8 +15,6 @@ import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, @@ -24,37 +23,39 @@ from .common import ( from tests.common import async_fire_time_changed -async def test_device_tracker(hass, config_entry, zha_gateway): +@pytest.fixture +def zigpy_device_dt(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: { + "in_clusters": [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.PollControl.cluster_id, + general.BinaryInput.cluster_id, + ], + "out_clusters": [general.Identify.cluster_id, general.Ota.cluster_id], + "device_type": SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, + } + } + return zigpy_device_mock(endpoints) + + +async def test_device_tracker( + hass, zha_gateway, zha_device_joined_restored, zigpy_device_dt +): """Test zha device tracker platform.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, - [ - general.Basic.cluster_id, - general.PowerConfiguration.cluster_id, - general.Identify.cluster_id, - general.PollControl.cluster_id, - general.BinaryInput.cluster_id, - ], - [general.Identify.cluster_id, general.Ota.cluster_id], - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, - zha_gateway, - ) - - # load up device tracker domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - - cluster = zigpy_device.endpoints.get(1).power - zha_device = zha_gateway.get_device(zigpy_device.ieee) + zha_device = await zha_device_joined_restored(zigpy_device_dt) + cluster = zigpy_device_dt.endpoints.get(1).power entity_id = await find_entity_id(DOMAIN, zha_device, hass) assert entity_id is not None # test that the device tracker was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - zigpy_device.last_seen = time.time() - 120 + zigpy_device_dt.last_seen = time.time() - 120 next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() @@ -73,7 +74,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway): attr = make_attribute(0x0021, 200) cluster.handle_message(hdr, [[attr]]) - zigpy_device.last_seen = time.time() + 10 + zigpy_device_dt.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() @@ -87,10 +88,12 @@ async def test_device_tracker(hass, config_entry, zha_gateway): assert entity.battery_level == 100 # test adding device tracker to the network and HA - await async_test_device_join( - hass, - zha_gateway, - general.PowerConfiguration.cluster_id, - entity_id, - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, - ) + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_device_dt) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_HOME + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == 2 + assert cluster.configure_reporting.await_count == 2 diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 973b6673671..56f5c6c85ba 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -3,13 +3,10 @@ import pytest import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation -from homeassistant.components.switch import DOMAIN -from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.components.zha.core.const import CHANNEL_EVENT_RELAY from homeassistant.helpers.device_registry import async_get_registry from homeassistant.setup import async_setup_component -from .common import async_enable_traffic, async_init_zigpy_device - from tests.common import async_get_device_automations, async_mock_service ON = 1 @@ -42,13 +39,31 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_triggers(hass, config_entry, zha_gateway): +@pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) +async def mock_devices(hass, zha_gateway, zigpy_device_mock, request): + """IAS device fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [general.Basic.cluster_id], + "out_clusters": [general.OnOff.cluster_id], + "device_type": 0, + } + }, + ) + + join_or_restore = request.getfixturevalue(request.param) + zha_device = await join_or_restore(zigpy_device) + zha_device.update_available(True) + await hass.async_block_till_done() + return zigpy_device, zha_device + + +async def test_triggers(hass, mock_devices): """Test zha device triggers.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway - ) + zigpy_device, zha_device = mock_devices zigpy_device.device_automation_triggers = { (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, @@ -58,11 +73,6 @@ async def test_triggers(hass, config_entry, zha_gateway): (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - zha_device = zha_gateway.get_device(zigpy_device.ieee) ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) @@ -110,19 +120,10 @@ async def test_triggers(hass, config_entry, zha_gateway): assert _same_lists(triggers, expected_triggers) -async def test_no_triggers(hass, config_entry, zha_gateway): +async def test_no_triggers(hass, mock_devices): """Test zha device with no triggers.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway - ) - - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - zha_device = zha_gateway.get_device(zigpy_device.ieee) + _, zha_device = mock_devices ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) @@ -132,13 +133,10 @@ async def test_no_triggers(hass, config_entry, zha_gateway): assert triggers == [] -async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): +async def test_if_fires_on_event(hass, mock_devices, calls): """Test for remote triggers firing.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway - ) + zigpy_device, zha_device = mock_devices zigpy_device.device_automation_triggers = { (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, @@ -148,15 +146,6 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - zha_device = zha_gateway.get_device(zigpy_device.ieee) - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) - ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) @@ -185,30 +174,18 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): await hass.async_block_till_done() - on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] - on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + channel = {ch.name: ch for ch in zha_device.all_channels}[CHANNEL_EVENT_RELAY] + channel.zha_send_event(channel.cluster, COMMAND_SINGLE, []) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["message"] == "service called" -async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog): +async def test_exception_no_triggers(hass, mock_devices, calls, caplog): """Test for exception on event triggers firing.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway - ) - - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - zha_device = zha_gateway.get_device(zigpy_device.ieee) - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + _, zha_device = mock_devices ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) @@ -239,13 +216,10 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, cap assert "Invalid config for [automation]" in caplog.text -async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog): +async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): """Test for exception on event triggers firing.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway - ) + zigpy_device, zha_device = mock_devices zigpy_device.device_automation_triggers = { (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, @@ -255,15 +229,6 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, cap (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - zha_device = zha_gateway.get_device(zigpy_device.ieee) - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) - ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 660bff2abac..48222993a3a 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,7 +1,7 @@ """Test zha fan.""" from unittest.mock import call, patch -import zigpy.zcl.clusters.general as general +import pytest import zigpy.zcl.clusters.hvac as hvac import zigpy.zcl.foundation as zcl_f @@ -18,8 +18,6 @@ from homeassistant.const import ( from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, @@ -28,20 +26,20 @@ from .common import ( from tests.common import mock_coro -async def test_fan(hass, config_entry, zha_gateway): +@pytest.fixture +def zigpy_device(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: {"in_clusters": [hvac.Fan.cluster_id], "out_clusters": [], "device_type": 0} + } + return zigpy_device_mock(endpoints) + + +async def test_fan(hass, zha_gateway, zha_device_joined_restored, zigpy_device): """Test zha fan platform.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [hvac.Fan.cluster_id, general.Basic.cluster_id], [], None, zha_gateway - ) - - # load up fan domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - + zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).fan - zha_device = zha_gateway.get_device(zigpy_device.ieee) entity_id = await find_entity_id(DOMAIN, zha_device, hass) assert entity_id is not None @@ -98,7 +96,14 @@ async def test_fan(hass, config_entry, zha_gateway): assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, entity_id) + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done() + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == 1 + assert cluster.configure_reporting.await_count == 1 async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 5c6e8ecfe7a..45f47a1fa87 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,29 +1,39 @@ """Test ZHA Gateway.""" +import pytest import zigpy.zcl.clusters.general as general -import homeassistant.components.zha.core.const as zha_const - -from .common import async_enable_traffic, async_init_zigpy_device +from .common import async_enable_traffic -async def test_device_left(hass, config_entry, zha_gateway): - """Test zha fan platform.""" - - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, [general.Basic.cluster_id], [], None, zha_gateway +@pytest.fixture +def zigpy_dev_basic(zigpy_device_mock): + """Zigpy device with just a basic cluster.""" + return zigpy_device_mock( + { + 1: { + "in_clusters": [general.Basic.cluster_id], + "out_clusters": [], + "device_type": 0, + } + }, ) - # load up fan domain - await hass.config_entries.async_forward_entry_setup(config_entry, zha_const.SENSOR) - await hass.async_block_till_done() - zha_device = zha_gateway.get_device(zigpy_device.ieee) +@pytest.fixture +async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): + """ZHA device with just a basic cluster.""" - assert zha_device.available is False + zha_device = await zha_device_restored(zigpy_dev_basic) + return zha_device - await async_enable_traffic(hass, zha_gateway, [zha_device]) - assert zha_device.available is True - zha_gateway.device_left(zigpy_device) - assert zha_device.available is False +async def test_device_left(hass, zha_gateway, zigpy_dev_basic, zha_dev_basic): + """Device leaving the network should become unavailable.""" + + assert zha_dev_basic.available is False + + await async_enable_traffic(hass, zha_gateway, [zha_dev_basic]) + assert zha_dev_basic.available is True + + zha_gateway.device_left(zigpy_dev_basic) + assert zha_dev_basic.available is False diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 53be188ae80..5df379d1d7a 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,10 +1,12 @@ """Test zha light.""" -import asyncio -from unittest.mock import MagicMock, call, patch, sentinel +from unittest.mock import call, sentinel +import asynctest +import pytest import zigpy.profiles.zha import zigpy.types import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import DOMAIN @@ -12,121 +14,124 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, ) -from tests.common import mock_coro - ON = 1 OFF = 0 +LIGHT_ON_OFF = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, + "in_clusters": [general.Basic.cluster_id, general.OnOff.cluster_id], + "out_clusters": [general.Ota.cluster_id], + } +} -async def test_light(hass, config_entry, zha_gateway, monkeypatch): +LIGHT_LEVEL = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT, + "in_clusters": [ + general.Basic.cluster_id, + general.LevelControl.cluster_id, + general.OnOff.cluster_id, + ], + "out_clusters": [general.Ota.cluster_id], + } +} + +LIGHT_COLOR = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT, + "in_clusters": [ + general.Basic.cluster_id, + general.LevelControl.cluster_id, + general.OnOff.cluster_id, + lighting.Color.cluster_id, + ], + "out_clusters": [general.Ota.cluster_id], + } +} + + +@asynctest.patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=asynctest.CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@asynctest.patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=asynctest.CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@asynctest.patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=asynctest.CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@pytest.mark.parametrize( + "device, reporting", + [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))], +) +async def test_light( + hass, zha_gateway, zigpy_device_mock, zha_device_joined_restored, device, reporting, +): """Test zha light platform.""" # create zigpy devices - zigpy_device_on_off = await async_init_zigpy_device( - hass, - [general.OnOff.cluster_id, general.Basic.cluster_id], - [], - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, - zha_gateway, - ieee="00:0d:6f:11:0a:90:69:e6", - ) + zigpy_device = zigpy_device_mock(device) + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) - zigpy_device_level = await async_init_zigpy_device( - hass, - [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - general.Basic.cluster_id, - ], - [], - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, - zha_gateway, - ieee="00:0d:6f:11:0a:90:69:e7", - manufacturer="FakeLevelManufacturer", - model="FakeLevelModel", - ) + assert entity_id is not None - # load up light domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - - # on off light - on_off_device_on_off_cluster = zigpy_device_on_off.endpoints.get(1).on_off - on_off_zha_device = zha_gateway.get_device(zigpy_device_on_off.ieee) - on_off_entity_id = await find_entity_id(DOMAIN, on_off_zha_device, hass) - assert on_off_entity_id is not None - - # dimmable light - level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off - level_device_level_cluster = zigpy_device_level.endpoints.get(1).level - on_off_mock = MagicMock( - side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) - ) - ) - level_mock = MagicMock( - side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) - ) - ) - monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock) - monkeypatch.setattr(level_device_level_cluster, "request", level_mock) - level_zha_device = zha_gateway.get_device(zigpy_device_level.ieee) - level_entity_id = await find_entity_id(DOMAIN, level_zha_device, hass) - assert level_entity_id is not None + cluster_on_off = zigpy_device.endpoints[1].on_off + cluster_level = getattr(zigpy_device.endpoints[1], "level", None) + cluster_color = getattr(zigpy_device.endpoints[1], "light_color", None) # test that the lights were created and that they are unavailable - assert hass.states.get(on_off_entity_id).state == STATE_UNAVAILABLE - assert hass.states.get(level_entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [on_off_zha_device, level_zha_device]) + await async_enable_traffic(hass, zha_gateway, [zha_device]) # test that the lights were created and are off - assert hass.states.get(on_off_entity_id).state == STATE_OFF - assert hass.states.get(level_entity_id).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF # test turning the lights on and off from the light - await async_test_on_off_from_light( - hass, on_off_device_on_off_cluster, on_off_entity_id - ) - - await async_test_on_off_from_light( - hass, level_device_on_off_cluster, level_entity_id - ) + await async_test_on_off_from_light(hass, cluster_on_off, entity_id) # test turning the lights on and off from the HA - await async_test_on_off_from_hass( - hass, on_off_device_on_off_cluster, on_off_entity_id - ) + await async_test_on_off_from_hass(hass, cluster_on_off, entity_id) - await async_test_level_on_off_from_hass( - hass, level_device_on_off_cluster, level_device_level_cluster, level_entity_id - ) + if cluster_level: + await async_test_level_on_off_from_hass( + hass, cluster_on_off, cluster_level, entity_id + ) - # test turning the lights on and off from the light - await async_test_on_from_light(hass, level_device_on_off_cluster, level_entity_id) + # test getting a brightness change from the network + await async_test_on_from_light(hass, cluster_on_off, entity_id) + await async_test_dimmer_from_light( + hass, cluster_level, entity_id, 150, STATE_ON + ) - # test getting a brightness change from the network - await async_test_dimmer_from_light( - hass, level_device_level_cluster, level_entity_id, 150, STATE_ON - ) - - # test adding a new light to the network and HA - await async_test_device_join( - hass, - zha_gateway, - general.OnOff.cluster_id, - on_off_entity_id, - device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, - ) + # test rejoin + await async_test_off_from_hass(hass, cluster_on_off, entity_id) + clusters = [cluster_on_off] + if cluster_level: + clusters.append(cluster_level) + if cluster_color: + clusters.append(cluster_color) + for cluster in clusters: + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + for cluster, reporting_count in zip(clusters, reporting): + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == reporting_count + assert cluster.configure_reporting.await_count == reporting_count async def async_test_on_off_from_light(hass, cluster, entity_id): @@ -157,36 +162,33 @@ async def async_test_on_from_light(hass, cluster, entity_id): async def async_test_on_off_from_hass(hass, cluster, entity_id): """Test on off functionality from hass.""" - with patch( - "zigpy.zcl.Cluster.request", - return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), - ): - # turn on via UI - await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.request.call_count == 1 - assert cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None - ) + # turn on via UI + cluster.request.reset_mock() + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert cluster.request.call_count == 1 + assert cluster.request.await_count == 1 + assert cluster.request.call_args == call( + False, ON, (), expect_reply=True, manufacturer=None + ) await async_test_off_from_hass(hass, cluster, entity_id) async def async_test_off_from_hass(hass, cluster, entity_id): """Test turning off the light from Home Assistant.""" - with patch( - "zigpy.zcl.Cluster.request", - return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), - ): - # turn off via UI - await hass.services.async_call( - DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.request.call_count == 1 - assert cluster.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None - ) + + # turn off via UI + cluster.request.reset_mock() + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert cluster.request.call_count == 1 + assert cluster.request.await_count == 1 + assert cluster.request.call_args == call( + False, OFF, (), expect_reply=True, manufacturer=None + ) async def async_test_level_on_off_from_hass( @@ -194,12 +196,15 @@ async def async_test_level_on_off_from_hass( ): """Test on off functionality from hass.""" + on_off_cluster.request.reset_mock() # turn on via UI await hass.services.async_call( DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert on_off_cluster.request.call_count == 1 + assert on_off_cluster.request.await_count == 1 assert level_cluster.request.call_count == 0 + assert level_cluster.request.await_count == 0 assert on_off_cluster.request.call_args == call( False, 1, (), expect_reply=True, manufacturer=None ) @@ -210,7 +215,9 @@ async def async_test_level_on_off_from_hass( DOMAIN, "turn_on", {"entity_id": entity_id, "transition": 10}, blocking=True ) assert on_off_cluster.request.call_count == 1 + assert on_off_cluster.request.await_count == 1 assert level_cluster.request.call_count == 1 + assert level_cluster.request.await_count == 1 assert on_off_cluster.request.call_args == call( False, 1, (), expect_reply=True, manufacturer=None ) @@ -230,7 +237,9 @@ async def async_test_level_on_off_from_hass( DOMAIN, "turn_on", {"entity_id": entity_id, "brightness": 10}, blocking=True ) assert on_off_cluster.request.call_count == 1 + assert on_off_cluster.request.await_count == 1 assert level_cluster.request.call_count == 1 + assert level_cluster.request.await_count == 1 assert on_off_cluster.request.call_args == call( False, 1, (), expect_reply=True, manufacturer=None ) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 1daef317fed..2c5dc9f41ba 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,6 +1,8 @@ """Test zha lock.""" from unittest.mock import patch +import pytest +import zigpy.profiles.zha import zigpy.zcl.clusters.closures as closures import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f @@ -10,7 +12,6 @@ from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED from .common import ( async_enable_traffic, - async_init_zigpy_device, find_entity_id, make_attribute, make_zcl_header, @@ -22,24 +23,29 @@ LOCK_DOOR = 0 UNLOCK_DOOR = 1 -async def test_lock(hass, config_entry, zha_gateway): - """Test zha lock platform.""" +@pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) +async def lock(hass, zha_gateway, zigpy_device_mock, request): + """Lock cluster fixture.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, - [closures.DoorLock.cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [closures.DoorLock.cluster_id, general.Basic.cluster_id], + "out_clusters": [], + "device_type": zigpy.profiles.zha.DeviceType.DOOR_LOCK, + } + }, ) - # load up lock domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() + join_or_restore = request.getfixturevalue(request.param) + zha_device = await join_or_restore(zigpy_device) + return zha_device, zigpy_device.endpoints[1].door_lock - cluster = zigpy_device.endpoints.get(1).door_lock - zha_device = zha_gateway.get_device(zigpy_device.ieee) + +async def test_lock(hass, zha_gateway, lock): + """Test zha lock platform.""" + + zha_device, cluster = lock entity_id = await find_entity_id(DOMAIN, zha_device, hass) assert entity_id is not None diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index b4fe0883bf3..b78ea8a3583 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -11,7 +11,6 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN import homeassistant.config as config_util from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, @@ -26,160 +25,43 @@ from homeassistant.util import dt as dt_util from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, ) -async def test_sensor(hass, config_entry, zha_gateway): - """Test zha sensor platform.""" - - # list of cluster ids to create devices and sensor entities for - cluster_ids = [ - measurement.RelativeHumidity.cluster_id, - measurement.TemperatureMeasurement.cluster_id, - measurement.PressureMeasurement.cluster_id, - measurement.IlluminanceMeasurement.cluster_id, - smartenergy.Metering.cluster_id, - homeautomation.ElectricalMeasurement.cluster_id, - ] - - # devices that were created from cluster_ids list above - zigpy_device_infos = await async_build_devices( - hass, zha_gateway, config_entry, cluster_ids - ) - - # ensure the sensor entity was created for each id in cluster_ids - for cluster_id in cluster_ids: - zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info[ATTR_ENTITY_ID] - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and devices - await async_enable_traffic( - hass, - zha_gateway, - [ - zigpy_device_info["zha_device"] - for zigpy_device_info in zigpy_device_infos.values() - ], - ) - - # test that the sensors now have a state of unknown - for cluster_id in cluster_ids: - zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info[ATTR_ENTITY_ID] - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - # get the humidity device info and test the associated sensor logic - device_info = zigpy_device_infos[measurement.RelativeHumidity.cluster_id] - await async_test_humidity(hass, device_info) - - # get the temperature device info and test the associated sensor logic - device_info = zigpy_device_infos[measurement.TemperatureMeasurement.cluster_id] - await async_test_temperature(hass, device_info) - - # get the pressure device info and test the associated sensor logic - device_info = zigpy_device_infos[measurement.PressureMeasurement.cluster_id] - await async_test_pressure(hass, device_info) - - # get the illuminance device info and test the associated sensor logic - device_info = zigpy_device_infos[measurement.IlluminanceMeasurement.cluster_id] - await async_test_illuminance(hass, device_info) - - # get the metering device info and test the associated sensor logic - device_info = zigpy_device_infos[smartenergy.Metering.cluster_id] - await async_test_metering(hass, device_info) - - # get the electrical_measurement device info and test the associated - # sensor logic - device_info = zigpy_device_infos[homeautomation.ElectricalMeasurement.cluster_id] - await async_test_electrical_measurement(hass, device_info) - - # test joining a new temperature sensor to the network - await async_test_device_join( - hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, entity_id - ) - - -async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): - """Build a zigpy device for each cluster id. - - This will build devices for all cluster ids that exist in cluster_ids. - They get added to the network and then the sensor component is loaded - which will cause sensor entities to get created for each device. - A dict containing relevant device info for testing is returned. It contains - the entity id, zigpy device, and the zigbee cluster for the sensor. - """ - - device_infos = {} - counter = 0 - for cluster_id in cluster_ids: - # create zigpy device - device_infos[cluster_id] = {"zigpy_device": None} - device_infos[cluster_id]["zigpy_device"] = await async_init_zigpy_device( - hass, - [cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, - ieee=f"00:15:8d:00:02:32:4f:0{counter}", - manufacturer=f"Fake{cluster_id}", - model=f"FakeModel{cluster_id}", - ) - - counter += 1 - - # load up sensor domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - - # put the other relevant info in the device info dict - for cluster_id in cluster_ids: - device_info = device_infos[cluster_id] - zigpy_device = device_info["zigpy_device"] - device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id] - zha_device = zha_gateway.get_device(zigpy_device.ieee) - device_info["zha_device"] = zha_device - device_info[ATTR_ENTITY_ID] = await find_entity_id(DOMAIN, zha_device, hass) - await hass.async_block_till_done() - return device_infos - - -async def async_test_humidity(hass, device_info): +async def async_test_humidity(hass, cluster, entity_id): """Test humidity sensor.""" - await send_attribute_report(hass, device_info["cluster"], 0, 1000) - assert_state(hass, device_info, "10.0", "%") + await send_attribute_report(hass, cluster, 0, 1000) + assert_state(hass, entity_id, "10.0", "%") -async def async_test_temperature(hass, device_info): +async def async_test_temperature(hass, cluster, entity_id): """Test temperature sensor.""" - await send_attribute_report(hass, device_info["cluster"], 0, 2900) - assert_state(hass, device_info, "29.0", "°C") + await send_attribute_report(hass, cluster, 0, 2900) + assert_state(hass, entity_id, "29.0", "°C") -async def async_test_pressure(hass, device_info): +async def async_test_pressure(hass, cluster, entity_id): """Test pressure sensor.""" - await send_attribute_report(hass, device_info["cluster"], 0, 1000) - assert_state(hass, device_info, "1000", "hPa") + await send_attribute_report(hass, cluster, 0, 1000) + assert_state(hass, entity_id, "1000", "hPa") -async def async_test_illuminance(hass, device_info): +async def async_test_illuminance(hass, cluster, entity_id): """Test illuminance sensor.""" - await send_attribute_report(hass, device_info["cluster"], 0, 10) - assert_state(hass, device_info, "1.0", "lx") + await send_attribute_report(hass, cluster, 0, 10) + assert_state(hass, entity_id, "1.0", "lx") -async def async_test_metering(hass, device_info): +async def async_test_metering(hass, cluster, entity_id): """Test metering sensor.""" - await send_attribute_report(hass, device_info["cluster"], 1024, 12345) - assert_state(hass, device_info, "12345.0", "unknown") + await send_attribute_report(hass, cluster, 1024, 12345) + assert_state(hass, entity_id, "12345.0", "unknown") -async def async_test_electrical_measurement(hass, device_info): +async def async_test_electrical_measurement(hass, cluster, entity_id): """Test electrical measurement sensor.""" with mock.patch( ( @@ -189,18 +71,81 @@ async def async_test_electrical_measurement(hass, device_info): new_callable=mock.PropertyMock, ) as divisor_mock: divisor_mock.return_value = 1 - await send_attribute_report(hass, device_info["cluster"], 1291, 100) - assert_state(hass, device_info, "100", "W") + await send_attribute_report(hass, cluster, 1291, 100) + assert_state(hass, entity_id, "100", "W") - await send_attribute_report(hass, device_info["cluster"], 1291, 99) - assert_state(hass, device_info, "99", "W") + await send_attribute_report(hass, cluster, 1291, 99) + assert_state(hass, entity_id, "99", "W") divisor_mock.return_value = 10 - await send_attribute_report(hass, device_info["cluster"], 1291, 1000) - assert_state(hass, device_info, "100", "W") + await send_attribute_report(hass, cluster, 1291, 1000) + assert_state(hass, entity_id, "100", "W") - await send_attribute_report(hass, device_info["cluster"], 1291, 99) - assert_state(hass, device_info, "9.9", "W") + await send_attribute_report(hass, cluster, 1291, 99) + assert_state(hass, entity_id, "9.9", "W") + + +@pytest.mark.parametrize( + "cluster_id, test_func, report_count", + ( + (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), + ( + homeautomation.ElectricalMeasurement.cluster_id, + async_test_electrical_measurement, + 1, + ), + ), +) +async def test_sensor( + hass, + zha_gateway, + zigpy_device_mock, + zha_device_joined_restored, + cluster_id, + test_func, + report_count, +): + """Test zha sensor platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [cluster_id, general.Basic.cluster_id], + "out_cluster": [], + "device_type": 0x0000, + } + } + ) + cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + + # ensure the sensor entity was created + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and devices + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the sensor now have a state of unknown + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + # test sensor associated logic + await test_func(hass, cluster, entity_id) + + # test rejoin + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done() + await test_func(hass, cluster, entity_id) + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == report_count + assert cluster.configure_reporting.await_count == report_count async def send_attribute_report(hass, cluster, attrid, value): @@ -215,13 +160,13 @@ async def send_attribute_report(hass, cluster, attrid, value): await hass.async_block_till_done() -def assert_state(hass, device_info, state, unit_of_measurement): +def assert_state(hass, entity_id, state, unit_of_measurement): """Check that the state is what is expected. This is used to ensure that the logic in each sensor class handled the attribute report it received correctly. """ - hass_state = hass.states.get(device_info[ATTR_ENTITY_ID]) + hass_state = hass.states.get(entity_id) assert hass_state.state == state assert hass_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement @@ -282,7 +227,15 @@ def core_rs(hass_storage): ], ) async def test_temp_uom( - uom, raw_temp, expected, restore, hass_ms, config_entry, zha_gateway, core_rs + uom, + raw_temp, + expected, + restore, + hass_ms, + zha_gateway, + core_rs, + zigpy_device_mock, + zha_device_restored, ): """Test zha temperature sensor unit of measurement.""" @@ -294,17 +247,22 @@ async def test_temp_uom( CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL ) - # list of cluster ids to create devices and sensor entities for - temp_cluster = measurement.TemperatureMeasurement - cluster_ids = [temp_cluster.cluster_id] - - # devices that were created from cluster_ids list above - zigpy_device_infos = await async_build_devices( - hass, zha_gateway, config_entry, cluster_ids + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + measurement.TemperatureMeasurement.cluster_id, + general.Basic.cluster_id, + ], + "out_cluster": [], + "device_type": 0x0000, + } + } ) + cluster = zigpy_device.endpoints[1].temperature + zha_device = await zha_device_restored(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) - zigpy_device_info = zigpy_device_infos[temp_cluster.cluster_id] - zha_device = zigpy_device_info["zha_device"] if not restore: assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -315,7 +273,7 @@ async def test_temp_uom( if not restore: assert hass.states.get(entity_id).state == STATE_UNKNOWN - await send_attribute_report(hass, zigpy_device_info["cluster"], 0, raw_temp) + await send_attribute_report(hass, cluster, 0, raw_temp) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state is not None diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 11a0b8f3481..f70538f65e8 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,6 +1,7 @@ """Test zha switch.""" from unittest.mock import call, patch +import pytest import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f @@ -9,8 +10,6 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, - async_init_zigpy_device, - async_test_device_join, find_entity_id, make_attribute, make_zcl_header, @@ -22,24 +21,24 @@ ON = 1 OFF = 0 -async def test_switch(hass, config_entry, zha_gateway): +@pytest.fixture +def zigpy_device(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: { + "in_clusters": [general.Basic.cluster_id, general.OnOff.cluster_id], + "out_clusters": [], + "device_type": 0, + } + } + return zigpy_device_mock(endpoints) + + +async def test_switch(hass, zha_gateway, zha_device_joined_restored, zigpy_device): """Test zha switch platform.""" - # create zigpy device - zigpy_device = await async_init_zigpy_device( - hass, - [general.OnOff.cluster_id, general.Basic.cluster_id], - [], - None, - zha_gateway, - ) - - # load up switch domain - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() - + zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).on_off - zha_device = zha_gateway.get_device(zigpy_device.ieee) entity_id = await find_entity_id(DOMAIN, zha_device, hass) assert entity_id is not None @@ -94,4 +93,11 @@ async def test_switch(hass, config_entry, zha_gateway): ) # test joining a new switch to the network and HA - await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, entity_id) + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + await zha_gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done() + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == 1 + assert cluster.configure_reporting.await_count == 1 From 12de3f1e4715ee2255571390d0f60b02aa5a5d1c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Feb 2020 19:47:16 -0800 Subject: [PATCH 177/378] Clean up frontend services and events (#31654) * Clean up frontend services and events * Fix bug in core instead * Add test that core works correctly with callback marked async funcs --- homeassistant/components/frontend/__init__.py | 54 ++++++++----------- homeassistant/core.py | 16 +++--- homeassistant/helpers/service.py | 4 +- tests/test_core.py | 25 +++++++++ 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index fdea21fe91e..b26d7a4e168 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -16,6 +16,7 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.config import async_hass_config_yaml from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass @@ -103,19 +104,6 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SET_THEME = "set_theme" SERVICE_RELOAD_THEMES = "reload_themes" -SERVICE_SET_THEME_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -WS_TYPE_GET_PANELS = "get_panels" -SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_PANELS} -) -WS_TYPE_GET_THEMES = "frontend/get_themes" -SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_THEMES} -) -WS_TYPE_GET_TRANSLATIONS = "frontend/get_translations" -SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_TRANSLATIONS, vol.Required("language"): str} -) class Panel: @@ -251,15 +239,9 @@ def _frontend_root(dev_repo_path): async def async_setup(hass, config): """Set up the serving of the frontend.""" await async_setup_frontend_storage(hass) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS - ) + hass.components.websocket_api.async_register_command(websocket_get_panels) + hass.components.websocket_api.async_register_command(websocket_get_themes) + hass.components.websocket_api.async_register_command(websocket_get_translations) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -331,11 +313,7 @@ async def async_setup(hass, config): def _async_setup_themes(hass, themes): """Set up themes data and services.""" hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME - if themes is None: - hass.data[DATA_THEMES] = {} - return - - hass.data[DATA_THEMES] = themes + hass.data[DATA_THEMES] = themes or {} @callback def update_theme_and_fire_event(): @@ -348,9 +326,7 @@ def _async_setup_themes(hass, themes): "app-header-background-color", themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), ) - hass.bus.async_fire( - EVENT_THEMES_UPDATED, {"themes": themes, "default_theme": name} - ) + hass.bus.async_fire(EVENT_THEMES_UPDATED) @callback def set_theme(call): @@ -373,10 +349,17 @@ def _async_setup_themes(hass, themes): hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME update_theme_and_fire_event() - hass.services.async_register( - DOMAIN, SERVICE_SET_THEME, set_theme, schema=SERVICE_SET_THEME_SCHEMA + service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_SET_THEME, + set_theme, + vol.Schema({vol.Required(CONF_NAME): cv.string}), + ) + + service.async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD_THEMES, reload_themes ) - hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) class IndexView(web_urldispatcher.AbstractResource): @@ -498,6 +481,7 @@ class ManifestJSONView(HomeAssistantView): @callback +@websocket_api.websocket_command({"type": "get_panels"}) def websocket_get_panels(hass, connection, msg): """Handle get panels command. @@ -514,6 +498,7 @@ def websocket_get_panels(hass, connection, msg): @callback +@websocket_api.websocket_command({"type": "frontend/get_themes"}) def websocket_get_themes(hass, connection, msg): """Handle get themes command. @@ -530,6 +515,9 @@ def websocket_get_themes(hass, connection, msg): ) +@websocket_api.websocket_command( + {"type": "frontend/get_translations", vol.Required("language"): str} +) @websocket_api.async_response async def websocket_get_translations(hass, connection, msg): """Handle get translations command. diff --git a/homeassistant/core.py b/homeassistant/core.py index 3f561cdfab8..e819a32b7c7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -298,10 +298,10 @@ class HomeAssistant: if asyncio.iscoroutine(check_target): task = self.loop.create_task(target) # type: ignore - elif is_callback(check_target): - self.loop.call_soon(target, *args) elif asyncio.iscoroutinefunction(check_target): task = self.loop.create_task(target(*args)) + elif is_callback(check_target): + self.loop.call_soon(target, *args) else: task = self.loop.run_in_executor( # type: ignore None, target, *args @@ -360,7 +360,11 @@ class HomeAssistant: target: target to call. args: parameters for method to call. """ - if not asyncio.iscoroutine(target) and is_callback(target): + if ( + not asyncio.iscoroutine(target) + and not asyncio.iscoroutinefunction(target) + and is_callback(target) + ): target(*args) else: self.async_add_job(target, *args) @@ -1245,10 +1249,10 @@ class ServiceRegistry: self, handler: Service, service_call: ServiceCall ) -> None: """Execute a service.""" - if handler.is_callback: - handler.func(service_call) - elif handler.is_coroutinefunction: + if handler.is_coroutinefunction: await handler.func(service_call) + elif handler.is_callback: + handler.func(service_call) else: await self._hass.async_add_executor_job(handler.func, service_call) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 46ebc467c0b..9085c929651 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -461,7 +461,9 @@ def async_register_admin_service( if not user.is_admin: raise Unauthorized(context=call.context) - await hass.async_add_job(service_func, call) + result = hass.async_add_job(service_func, call) + if result is not None: + await result hass.services.async_register(domain, service, admin_handler, schema) diff --git a/tests/test_core.py b/tests/test_core.py index aa0c615ec04..657bbeda7c6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1180,3 +1180,28 @@ def test_context(): assert c.user_id == 23 assert c.parent_id == 100 assert c.id is not None + + +async def test_async_functions_with_callback(hass): + """Test we deal with async functions accidentally marked as callback.""" + runs = [] + + @ha.callback + async def test(): + runs.append(True) + + await hass.async_add_job(test) + assert len(runs) == 1 + + hass.async_run_job(test) + await hass.async_block_till_done() + assert len(runs) == 2 + + @ha.callback + async def service_handler(call): + runs.append(True) + + hass.services.async_register("test_domain", "test_service", service_handler) + + await hass.services.async_call("test_domain", "test_service", blocking=True) + assert len(runs) == 3 From 5092971452afc44b9a82431fa318a9c4625ad7f4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Feb 2020 19:47:59 -0800 Subject: [PATCH 178/378] Add brightness light device actions (#31567) --- .../device_automation/toggle_entity.py | 4 +- .../components/light/device_action.py | 77 +++++++++++++++++-- tests/components/light/test_device_action.py | 62 ++++++++++++++- 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index f6bb74edbec..a2dcd62db8c 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -74,10 +74,12 @@ ENTITY_TRIGGERS = [ }, ] +DEVICE_ACTION_TYPES = [CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON] + ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), + vol.Required(CONF_TYPE): vol.In(DEVICE_ACTION_TYPES), } ) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index c436ce7886a..99f5b6b12bc 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -4,13 +4,32 @@ from typing import List import voluptuous as vol from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_DOMAIN, + CONF_TYPE, + SERVICE_TURN_ON, +) from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import DOMAIN +from . import ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) +TYPE_BRIGHTNESS_INCREASE = "brightness_increase" +TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_TYPE): vol.In( + toggle_entity.DEVICE_ACTION_TYPES + + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE] + ), + } +) async def async_call_action_from_config( @@ -20,11 +39,57 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN + if config[CONF_TYPE] in toggle_entity.DEVICE_ACTION_TYPES: + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + return + + data = {ATTR_ENTITY_ID: config[ATTR_ENTITY_ID]} + + if config[CONF_TYPE] == TYPE_BRIGHTNESS_INCREASE: + data[ATTR_BRIGHTNESS_STEP_PCT] = 10 + else: + data[ATTR_BRIGHTNESS_STEP_PCT] = -10 + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context ) async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + registry = await entity_registry.async_get_registry(hass) + + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + if state: + supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + else: + supported_features = entry.supported_features + + if supported_features & SUPPORT_BRIGHTNESS: + actions.extend( + ( + { + CONF_TYPE: TYPE_BRIGHTNESS_INCREASE, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + }, + { + CONF_TYPE: TYPE_BRIGHTNESS_DECREASE, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + }, + ) + ) + + return actions diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index a737396cca8..3ac8171ce7d 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -2,7 +2,7 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.light import DOMAIN +from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -42,7 +42,13 @@ async def test_get_actions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=SUPPORT_BRIGHTNESS, + ) expected_actions = [ { "domain": DOMAIN, @@ -62,6 +68,18 @@ async def test_get_actions(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "domain": DOMAIN, + "type": "brightness_increase", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "brightness_decrease", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, ] actions = await async_get_device_automations(hass, "action", device_entry.id) assert actions == expected_actions @@ -108,6 +126,30 @@ async def test_action(hass, calls): "type": "toggle", }, }, + { + "trigger": { + "platform": "event", + "event_type": "test_brightness_increase", + }, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "brightness_increase", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_brightness_decrease", + }, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "brightness_decrease", + }, + }, ] }, ) @@ -138,3 +180,19 @@ async def test_action(hass, calls): hass.bus.async_fire("test_event3") await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_ON + + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + + hass.bus.async_fire("test_brightness_increase") + await hass.async_block_till_done() + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].data["entity_id"] == ent1.entity_id + assert turn_on_calls[0].data["brightness_step_pct"] == 10 + + hass.bus.async_fire("test_brightness_decrease") + await hass.async_block_till_done() + + assert len(turn_on_calls) == 2 + assert turn_on_calls[1].data["entity_id"] == ent1.entity_id + assert turn_on_calls[1].data["brightness_step_pct"] == -10 From 7d0b50cadb411340576fdec74686adb44e269774 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 10 Feb 2020 12:33:07 +0100 Subject: [PATCH 179/378] Fix wrong error message in netatmo integration (#31690) --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 82c3748d19b..afdb7c053f3 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -217,7 +217,7 @@ class NetatmoSensor(Entity): if data is None: _LOGGER.info("No data found for %s (%s)", self.module_name, self._module_id) - _LOGGER.error("data: %s", self.netatmo_data.data) + _LOGGER.debug("data: %s", self.netatmo_data.data) self._state = None return From 9e41ee49cbe06bb53bbd5c6d5f3c17a009111b25 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 10 Feb 2020 17:00:22 +0100 Subject: [PATCH 180/378] =?UTF-8?q?Fix=20M=C3=A9t=C3=A9o-France=20I/O=20wh?= =?UTF-8?q?ile=20testing=20(#31695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Météo-France I/O while testing * Review --- tests/components/meteo_france/conftest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/components/meteo_france/conftest.py diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py new file mode 100644 index 00000000000..088587ab2c2 --- /dev/null +++ b/tests/components/meteo_france/conftest.py @@ -0,0 +1,16 @@ +"""Meteo-France generic test utils.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def patch_requests(): + """Stub out services that makes requests.""" + patch_client = patch("homeassistant.components.meteo_france.meteofranceClient") + patch_weather_alert = patch( + "homeassistant.components.meteo_france.VigilanceMeteoFranceProxy" + ) + + with patch_client, patch_weather_alert: + yield From 5a7e0b84ff0bf10b21bbc5ed0b98091d7283b6c5 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 10 Feb 2020 21:22:09 +0100 Subject: [PATCH 181/378] Bump velbus version + load velbus module names into device info (#31664) --- homeassistant/components/velbus/__init__.py | 6 +++--- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index d8f9dae13de..b4fe49a88e7 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -140,11 +140,11 @@ class VelbusEntity(Entity): "identifiers": { (DOMAIN, self._module.get_module_address(), self._module.serial) }, - "name": "{} {}".format( - self._module.get_module_address(), self._module.get_module_name() + "name": "{} ({})".format( + self._module.get_module_name(), self._module.get_module_address() ), "manufacturer": "Velleman", - "model": self._module.get_module_name(), + "model": self._module.get_module_type_name(), "sw_version": "{}.{}-{}".format( self._module.memory_map_version, self._module.build_year, diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 258b367fa5b..f74f1b9b784 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.0.36"], + "requirements": ["python-velbus==2.0.40"], "config_flow": true, "dependencies": [], "codeowners": ["@Cereal2nd", "@brefra"] diff --git a/requirements_all.txt b/requirements_all.txt index 3a46e3514e4..0421344be0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1636,7 +1636,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.36 +python-velbus==2.0.40 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4fa6515bab..85022c10dc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ python-miio==0.4.8 python-nest==4.1.0 # homeassistant.components.velbus -python-velbus==2.0.36 +python-velbus==2.0.40 # homeassistant.components.awair python_awair==0.0.4 From 7e0560c7dccc7ccae79fd640c23638ce82328bd6 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 10 Feb 2020 21:23:02 +0100 Subject: [PATCH 182/378] Vicare water_heater set_temperature fix and bump PyVicare to 0.1.7 (#31672) * Fix ViCare water_heater set_temperature This fixes a obvious but undiscovered bug in the water heater component: Instead of the commanded value the prvious value was set on the API * Bump PyVicare to 0.1.7 --- homeassistant/components/vicare/manifest.json | 2 +- homeassistant/components/vicare/water_heater.py | 3 ++- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index cefc244e5b8..66fd15d3a90 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.1.2"] + "requirements": ["PyViCare==0.1.7"] } diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index f31e4f65170..eea3d81faf6 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -122,7 +122,8 @@ class ViCareWater(WaterHeaterDevice): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: - self._api.setDomesticHotWaterTemperature(self._target_temperature) + self._api.setDomesticHotWaterTemperature(temp) + self._target_temperature = temp @property def min_temp(self): diff --git a/requirements_all.txt b/requirements_all.txt index 0421344be0a..7079f858245 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -77,7 +77,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.vicare -PyViCare==0.1.2 +PyViCare==0.1.7 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4 From b78d156f0ea65ef53a2aad6948fa5fff06809a1a Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Mon, 10 Feb 2020 23:09:12 +0200 Subject: [PATCH 183/378] Add MELCloud integration (#30712) * Add MELCloud integration * Provides a climate and sensor platforms. Multiple platforms on one go is not the best option, but it does not make sense to remove them and commit them later either. * Email and access token are stored to the ConfigEntry. The token can be updated by adding the integration again with the same email address. The config flow is aborted and the update is performed on the background. * Run isort * Fix pylint errors * Run black * Increase coverage * Update pymelcloud dependency * Add HVAC_MODE_OFF emulation * Remove print * Update pymelcloud to enable device type filtering * Collapse except blocks and chain ClientNotReadys * Add preliminary documentation URL * Use list comp for creating model info Filters out empty model names form units. * f-string galore Dropped 'HVAC' from AtaDevice name template. * Delegate fan mode mapping to pymelcloud * Fix type annotation * Access AtaDevice through self._device * Prefer list comprehension * Update pymelcloud to leverage device type grouping The updated backend lib returns devices in a dict grouped by the device type. The devices do not necessarily need to be in a single list and this way isinstance is not required to extract devices by type. * Remove DOMAIN presence check This does not seem to make much sense after all. * Fix async_setup_entry Entry setup used half-baked naming from few experimentations back. The naming conventiens were unified to match the platforms. A redundant noneness check was also removed after evaluating the possible return values from the backend lib. * Simplify empty model name check * Improve config validation * Use config_validation strings. * Add CONF_EMAIL to config schema. The value is not strictly required when configuring through configuration.yaml, but having it there makes things more consistent. * Use dict[key] to access required properties. * Add DOMAIN in config check back to async_setup. This is required if an integration is configured throught config_flow. * Remove unused manifest properties * Remove redundant ClimateDevice property override * Add __init__.py to coverage exclusion * Use CONF_USERNAME instead of CONF_EMAIL * Use asyncio.gather instead of asyncio.wait * Misc fixes * any -> Any * Better names for dict iterations * Proper dict access with mandatory/known keys * Remove unused 'name' argument * Remove unnecessary platform info from unique_ids * Remove redundant methods from climate platform * Remove redundant default value from dict get * Update ConfigFlow sub-classing * Define sensors in a dict instead of a list * Use _abort_if_unique_id_configured to update token * Fix them tests * Remove current state guards * Fix that gather call * Implement sensor definitions without str manipulation * Use relative intra-package imports * Update homeassistant/components/melcloud/config_flow.py Co-Authored-By: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/melcloud/.translations/en.json | 23 +++ homeassistant/components/melcloud/__init__.py | 160 ++++++++++++++++ homeassistant/components/melcloud/climate.py | 171 ++++++++++++++++++ .../components/melcloud/config_flow.py | 84 +++++++++ homeassistant/components/melcloud/const.py | 29 +++ .../components/melcloud/manifest.json | 9 + homeassistant/components/melcloud/sensor.py | 98 ++++++++++ .../components/melcloud/strings.json | 23 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/melcloud/__init__.py | 1 + tests/components/melcloud/test_config_flow.py | 171 ++++++++++++++++++ 15 files changed, 780 insertions(+) create mode 100644 homeassistant/components/melcloud/.translations/en.json create mode 100644 homeassistant/components/melcloud/__init__.py create mode 100644 homeassistant/components/melcloud/climate.py create mode 100644 homeassistant/components/melcloud/config_flow.py create mode 100644 homeassistant/components/melcloud/const.py create mode 100644 homeassistant/components/melcloud/manifest.json create mode 100644 homeassistant/components/melcloud/sensor.py create mode 100644 homeassistant/components/melcloud/strings.json create mode 100644 tests/components/melcloud/__init__.py create mode 100644 tests/components/melcloud/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 369aaa3b4e0..bd9bf196321 100644 --- a/.coveragerc +++ b/.coveragerc @@ -410,6 +410,9 @@ omit = homeassistant/components/mcp23017/* homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py + homeassistant/components/melcloud/__init__.py + homeassistant/components/melcloud/climate.py + homeassistant/components/melcloud/sensor.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py homeassistant/components/meteo_france/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index c3f018ef83a..46c3d416b5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -204,6 +204,7 @@ homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mediaroom/* @dgomes +homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame diff --git a/homeassistant/components/melcloud/.translations/en.json b/homeassistant/components/melcloud/.translations/en.json new file mode 100644 index 00000000000..477ca7eb5e2 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "MELCloud", + "step": { + "user": { + "title": "Connect to MELCloud", + "description": "Connect using your MELCloud account.", + "data": { + "username": "Email used to login to MELCloud.", + "password": "MELCloud password." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + } + } +} diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py new file mode 100644 index 00000000000..ef932f36aa4 --- /dev/null +++ b/homeassistant/components/melcloud/__init__.py @@ -0,0 +1,160 @@ +"""The MELCloud Climate integration.""" +import asyncio +from datetime import timedelta +import logging +from typing import Any, Dict, List + +from aiohttp import ClientConnectionError +from async_timeout import timeout +from pymelcloud import Device, get_devices +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import Throttle + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +PLATFORMS = ["climate", "sensor"] + +CONF_LANGUAGE = "language" +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_TOKEN): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigEntry): + """Establish connection with MELCloud.""" + if DOMAIN not in config: + return True + + username = config[DOMAIN][CONF_USERNAME] + token = config[DOMAIN][CONF_TOKEN] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: username, CONF_TOKEN: token}, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Establish connection with MELClooud.""" + conf = entry.data + mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return True + + +class MelCloudDevice: + """MELCloud Device instance.""" + + def __init__(self, device: Device): + """Construct a device wrapper.""" + self.device = device + self.name = device.name + self._available = True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self, **kwargs): + """Pull the latest data from MELCloud.""" + try: + await self.device.update() + self._available = True + except ClientConnectionError: + _LOGGER.warning("Connection failed for %s", self.name) + self._available = False + + async def async_set(self, properties: Dict[str, Any]): + """Write state changes to the MELCloud API.""" + try: + await self.device.set(properties) + self._available = True + except ClientConnectionError: + _LOGGER.warning("Connection failed for %s", self.name) + self._available = False + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_id(self): + """Return device ID.""" + return self.device.device_id + + @property + def building_id(self): + """Return building ID of the device.""" + return self.device.building_id + + @property + def device_info(self): + """Return a device description for device registry.""" + _device_info = { + "identifiers": {(DOMAIN, f"{self.device.mac}-{self.device.serial}")}, + "manufacturer": "Mitsubishi Electric", + "name": self.name, + } + unit_infos = self.device.units + if unit_infos is not None: + _device_info["model"] = ", ".join( + [x["model"] for x in unit_infos if x["model"]] + ) + return _device_info + + +async def mel_devices_setup(hass, token) -> List[MelCloudDevice]: + """Query connected devices from MELCloud.""" + session = hass.helpers.aiohttp_client.async_get_clientsession() + try: + with timeout(10): + all_devices = await get_devices( + token, + session, + conf_update_interval=timedelta(minutes=5), + device_set_debounce=timedelta(seconds=1), + ) + except (asyncio.TimeoutError, ClientConnectionError) as ex: + raise ConfigEntryNotReady() from ex + + wrapped_devices = {} + for device_type, devices in all_devices.items(): + wrapped_devices[device_type] = [MelCloudDevice(device) for device in devices] + return wrapped_devices diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py new file mode 100644 index 00000000000..95cb1489f45 --- /dev/null +++ b/homeassistant/components/melcloud/climate.py @@ -0,0 +1,171 @@ +"""Platform for climate integration.""" +from datetime import timedelta +import logging +from typing import List, Optional + +from pymelcloud import DEVICE_TYPE_ATA + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.temperature import convert as convert_temperature + +from . import MelCloudDevice +from .const import DOMAIN, HVAC_MODE_LOOKUP, HVAC_MODE_REVERSE_LOOKUP, TEMP_UNIT_LOOKUP + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): + """Set up MelCloud device climate based on config_entry.""" + mel_devices = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [AtaDeviceClimate(mel_device) for mel_device in mel_devices[DEVICE_TYPE_ATA]], + True, + ) + + +class AtaDeviceClimate(ClimateDevice): + """Air-to-Air climate device.""" + + def __init__(self, device: MelCloudDevice): + """Initialize the climate.""" + self._api = device + self._device = self._api.device + self._name = device.name + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._device.serial}-{self._device.mac}" + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + async def async_update(self): + """Update state from MELCloud.""" + await self._api.async_update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement used by the platform.""" + return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS) + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + mode = self._device.operation_mode + if not self._device.power or mode is None: + return HVAC_MODE_OFF + return HVAC_MODE_LOOKUP.get(mode) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self._device.set({"power": False}) + return + + operation_mode = HVAC_MODE_REVERSE_LOOKUP.get(hvac_mode) + if operation_mode is None: + raise ValueError(f"Invalid hvac_mode [{hvac_mode}]") + + props = {"operation_mode": operation_mode} + if self.hvac_mode == HVAC_MODE_OFF: + props["power"] = True + await self._device.set(props) + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_OFF] + [ + HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes + ] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._device.room_temperature + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + return self._device.target_temperature + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self._device.set( + {"target_temperature": kwargs.get("temperature", self.target_temperature)} + ) + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return self._device.target_temperature_step + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return self._device.fan_speed + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + await self._device.set({"fan_speed": fan_mode}) + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return self._device.fan_speeds + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self._device.set({"power": True}) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self._device.set({"power": False}) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + min_value = self._device.target_temperature_min + if min_value is not None: + return min_value + + return convert_temperature( + DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit + ) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + max_value = self._device.target_temperature_max + if max_value is not None: + return max_value + + return convert_temperature( + DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit + ) diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py new file mode 100644 index 00000000000..6bda8cc3c28 --- /dev/null +++ b/homeassistant/components/melcloud/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for the MELCloud platform.""" +import asyncio +import logging +from typing import Optional + +from aiohttp import ClientError, ClientResponseError +from async_timeout import timeout +import pymelcloud +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME + +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _create_entry(self, username: str, token: str): + """Register new entry.""" + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured({CONF_TOKEN: token}) + return self.async_create_entry( + title=username, data={CONF_USERNAME: username, CONF_TOKEN: token}, + ) + + async def _create_client( + self, + username: str, + *, + password: Optional[str] = None, + token: Optional[str] = None, + ): + """Create client.""" + if password is None and token is None: + raise ValueError( + "Invalid internal state. Called without either password or token", + ) + + try: + with timeout(10): + acquired_token = token + if acquired_token is None: + acquired_token = await pymelcloud.login( + username, + password, + self.hass.helpers.aiohttp_client.async_get_clientsession(), + ) + await pymelcloud.get_devices( + acquired_token, + self.hass.helpers.aiohttp_client.async_get_clientsession(), + ) + except ClientResponseError as err: + if err.status == 401 or err.status == 403: + return self.async_abort(reason="invalid_auth") + return self.async_abort(reason="cannot_connect") + except (asyncio.TimeoutError, ClientError): + return self.async_abort(reason="cannot_connect") + + return await self._create_entry(username, acquired_token) + + async def async_step_user(self, user_input=None): + """User initiated config flow.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + ) + username = user_input[CONF_USERNAME] + return await self._create_client(username, password=user_input[CONF_PASSWORD]) + + async def async_step_import(self, user_input): + """Import a config entry.""" + return await self._create_client( + user_input[CONF_USERNAME], token=user_input[CONF_TOKEN] + ) diff --git a/homeassistant/components/melcloud/const.py b/homeassistant/components/melcloud/const.py new file mode 100644 index 00000000000..e262be2c3fb --- /dev/null +++ b/homeassistant/components/melcloud/const.py @@ -0,0 +1,29 @@ +"""Constants for the MELCloud Climate integration.""" +import pymelcloud.ata_device as ata_device +from pymelcloud.const import UNIT_TEMP_CELSIUS, UNIT_TEMP_FAHRENHEIT + +from homeassistant.components.climate.const import ( + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, +) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +DOMAIN = "melcloud" + +HVAC_MODE_LOOKUP = { + ata_device.OPERATION_MODE_HEAT: HVAC_MODE_HEAT, + ata_device.OPERATION_MODE_DRY: HVAC_MODE_DRY, + ata_device.OPERATION_MODE_COOL: HVAC_MODE_COOL, + ata_device.OPERATION_MODE_FAN_ONLY: HVAC_MODE_FAN_ONLY, + ata_device.OPERATION_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL, +} +HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in HVAC_MODE_LOOKUP.items()} + +TEMP_UNIT_LOOKUP = { + UNIT_TEMP_CELSIUS: TEMP_CELSIUS, + UNIT_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT, +} +TEMP_UNIT_REVERSE_LOOKUP = {v: k for k, v in TEMP_UNIT_LOOKUP.items()} diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json new file mode 100644 index 00000000000..43331def303 --- /dev/null +++ b/homeassistant/components/melcloud/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "melcloud", + "name": "MELCloud", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/melcloud", + "requirements": ["pymelcloud==2.0.0"], + "dependencies": [], + "codeowners": ["@vilppuvuorinen"] +} diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py new file mode 100644 index 00000000000..428c83a4ee3 --- /dev/null +++ b/homeassistant/components/melcloud/sensor.py @@ -0,0 +1,98 @@ +"""Support for MelCloud device sensors.""" +import logging + +from pymelcloud import DEVICE_TYPE_ATA, AtaDevice + +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.entity import Entity +from homeassistant.util.unit_system import UnitSystem + +from .const import DOMAIN, TEMP_UNIT_LOOKUP + +ATTR_MEASUREMENT_NAME = "measurement_name" +ATTR_ICON = "icon" +ATTR_UNIT_FN = "unit_fn" +ATTR_DEVICE_CLASS = "device_class" +ATTR_VALUE_FN = "value_fn" + +SENSORS = { + "room_temperature": { + ATTR_MEASUREMENT_NAME: "Room Temperature", + ATTR_ICON: "mdi:thermometer", + ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_VALUE_FN: lambda x: x.device.room_temperature, + }, + "energy": { + ATTR_MEASUREMENT_NAME: "Energy", + ATTR_ICON: "mdi:factory", + ATTR_UNIT_FN: lambda x: "kWh", + ATTR_DEVICE_CLASS: None, + ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed, + }, +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up MELCloud device sensors based on config_entry.""" + mel_devices = hass.data[DOMAIN].get(entry.entry_id) + async_add_entities( + [ + MelCloudSensor(mel_device, measurement, definition, hass.config.units) + for measurement, definition in SENSORS.items() + for mel_device in mel_devices[DEVICE_TYPE_ATA] + ], + True, + ) + + +class MelCloudSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, device: AtaDevice, measurement, definition, units: UnitSystem): + """Initialize the sensor.""" + self._api = device + self._name_slug = device.name + self._measurement = measurement + self._def = definition + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._api.device.serial}-{self._api.device.mac}-{self._measurement}" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._def[ATTR_ICON] + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name_slug} {self._def[ATTR_MEASUREMENT_NAME]}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._def[ATTR_VALUE_FN](self._api) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._def[ATTR_UNIT_FN](self._api) + + @property + def device_class(self): + """Return device class.""" + return self._def[ATTR_DEVICE_CLASS] + + async def async_update(self): + """Retrieve latest state.""" + await self._api.async_update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json new file mode 100644 index 00000000000..477ca7eb5e2 --- /dev/null +++ b/homeassistant/components/melcloud/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "MELCloud", + "step": { + "user": { + "title": "Connect to MELCloud", + "description": "Connect using your MELCloud account.", + "data": { + "username": "Email used to login to MELCloud.", + "password": "MELCloud password." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4872b08e9fc..c77a8de9388 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -54,6 +54,7 @@ FLOWS = [ "logi_circle", "luftdaten", "mailgun", + "melcloud", "met", "meteo_france", "mikrotik", diff --git a/requirements_all.txt b/requirements_all.txt index 7079f858245..acfb089c203 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1354,6 +1354,9 @@ pymailgunner==1.4 # homeassistant.components.mediaroom pymediaroom==0.6.4 +# homeassistant.components.melcloud +pymelcloud==2.0.0 + # homeassistant.components.somfy pymfy==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85022c10dc2..af22deb6b6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -484,6 +484,9 @@ pylitejet==0.1 # homeassistant.components.mailgun pymailgunner==1.4 +# homeassistant.components.melcloud +pymelcloud==2.0.0 + # homeassistant.components.somfy pymfy==0.7.1 diff --git a/tests/components/melcloud/__init__.py b/tests/components/melcloud/__init__.py new file mode 100644 index 00000000000..f20383660d4 --- /dev/null +++ b/tests/components/melcloud/__init__.py @@ -0,0 +1 @@ +"""Tests for the MELCloud integration.""" diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py new file mode 100644 index 00000000000..90c766f0831 --- /dev/null +++ b/tests/components/melcloud/test_config_flow.py @@ -0,0 +1,171 @@ +"""Test the MELCloud config flow.""" +import asyncio + +from aiohttp import ClientError, ClientResponseError +from asynctest import patch +import pymelcloud +import pytest + +from homeassistant import config_entries +from homeassistant.components.melcloud.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_login(): + """Mock pymelcloud login.""" + with patch("pymelcloud.login") as mock: + mock.return_value = "test-token" + yield mock + + +@pytest.fixture +def mock_get_devices(): + """Mock pymelcloud get_devices.""" + with patch("pymelcloud.get_devices") as mock: + mock.return_value = { + pymelcloud.DEVICE_TYPE_ATA: [], + pymelcloud.DEVICE_TYPE_ATW: [], + } + yield mock + + +@pytest.fixture +def mock_request_info(): + """Mock RequestInfo to create ClientResponseErrors.""" + with patch("aiohttp.RequestInfo") as mock_ri: + mock_ri.return_value.real_url.return_value = "" + yield mock_ri + + +async def test_form(hass, mock_login, mock_get_devices): + """Test we get the form.""" + 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.melcloud.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.melcloud.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-email@test-domain.com", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-email@test-domain.com" + assert result2["data"] == { + "username": "test-email@test-domain.com", + "token": "test-token", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error,reason", + [(ClientError(), "cannot_connect"), (asyncio.TimeoutError(), "cannot_connect")], +) +async def test_form_errors(hass, mock_login, mock_get_devices, error, reason): + """Test we handle cannot connect error.""" + mock_login.side_effect = error + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"username": "test-email@test-domain.com", "password": "test-password"}, + ) + + assert len(mock_login.mock_calls) == 1 + assert result["type"] == "abort" + assert result["reason"] == reason + + +@pytest.mark.parametrize( + "error,message", + [(401, "invalid_auth"), (403, "invalid_auth"), (500, "cannot_connect")], +) +async def test_form_response_errors( + hass, mock_login, mock_get_devices, mock_request_info, error, message +): + """Test we handle response errors.""" + mock_login.side_effect = ClientResponseError(mock_request_info(), (), status=error) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"username": "test-email@test-domain.com", "password": "test-password"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == message + + +async def test_import_with_token(hass, mock_login, mock_get_devices): + """Test successful import.""" + with patch( + "homeassistant.components.melcloud.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.melcloud.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"username": "test-email@test-domain.com", "token": "test-token"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "test-email@test-domain.com" + assert result["data"] == { + "username": "test-email@test-domain.com", + "token": "test-token", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_token_refresh(hass, mock_login, mock_get_devices): + """Re-configuration with existing username should refresh token.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "test-email@test-domain.com", + "token": "test-original-token", + }, + unique_id="test-email@test-domain.com", + ) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.melcloud.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.melcloud.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + "username": "test-email@test-domain.com", + "password": "test-password", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + entry = entries[0] + assert entry.data["username"] == "test-email@test-domain.com" + assert entry.data["token"] == "test-token" From 4467409e5c1dca41b0c9e17a3c381cbf43afcbbe Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Mon, 10 Feb 2020 23:16:04 +0200 Subject: [PATCH 184/378] Dynalite Integration (#27841) * Initial commit * ran hassfest and gen_requirements_all scripts * fixes per request from Paulus Schoutsen * ran gen_requirements_all * updated library version - removed some debug leftover * get_requirements again... * added documentation URL * ran isort * changed storage in hass.data[DOMAIN] to use entry_id instead of host * adopted unit tests to latest fix * Update const.py Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/dynalite/__init__.py | 93 ++++++++++++ homeassistant/components/dynalite/bridge.py | 118 +++++++++++++++ .../components/dynalite/config_flow.py | 58 ++++++++ homeassistant/components/dynalite/const.py | 11 ++ homeassistant/components/dynalite/light.py | 84 +++++++++++ .../components/dynalite/manifest.json | 9 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/dynalite/__init__.py | 1 + tests/components/dynalite/test_bridge.py | 136 ++++++++++++++++++ tests/components/dynalite/test_config_flow.py | 36 +++++ tests/components/dynalite/test_init.py | 74 ++++++++++ tests/components/dynalite/test_light.py | 44 ++++++ 15 files changed, 672 insertions(+) create mode 100755 homeassistant/components/dynalite/__init__.py create mode 100755 homeassistant/components/dynalite/bridge.py create mode 100755 homeassistant/components/dynalite/config_flow.py create mode 100755 homeassistant/components/dynalite/const.py create mode 100755 homeassistant/components/dynalite/light.py create mode 100755 homeassistant/components/dynalite/manifest.json create mode 100755 tests/components/dynalite/__init__.py create mode 100755 tests/components/dynalite/test_bridge.py create mode 100755 tests/components/dynalite/test_config_flow.py create mode 100755 tests/components/dynalite/test_init.py create mode 100755 tests/components/dynalite/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 46c3d416b5d..8f44c3caebc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -83,6 +83,7 @@ homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff +homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dyson/* @etheralm homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py new file mode 100755 index 00000000000..cb6b52483b7 --- /dev/null +++ b/homeassistant/components/dynalite/__init__.py @@ -0,0 +1,93 @@ +"""Support for the Dynalite networks.""" +from dynalite_devices_lib import BRIDGE_CONFIG_SCHEMA +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.helpers import config_validation as cv + +# Loading the config flow file will register the flow +from .bridge import DynaliteBridge +from .config_flow import configured_hosts +from .const import CONF_BRIDGES, DATA_CONFIGS, DOMAIN, LOGGER + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_BRIDGES): vol.All( + cv.ensure_list, [BRIDGE_CONFIG_SCHEMA] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Dynalite platform.""" + + conf = config.get(DOMAIN) + LOGGER.debug("Setting up dynalite component config = %s", conf) + + if conf is None: + conf = {} + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CONFIGS] = {} + + configured = configured_hosts(hass) + + # User has configured bridges + if CONF_BRIDGES not in conf: + return True + + bridges = conf[CONF_BRIDGES] + + for bridge_conf in bridges: + host = bridge_conf[CONF_HOST] + LOGGER.debug("async_setup host=%s conf=%s", host, bridge_conf) + + # Store config in hass.data so the config entry can find it + hass.data[DOMAIN][DATA_CONFIGS][host] = bridge_conf + + if host in configured: + LOGGER.debug("async_setup host=%s already configured", host) + continue + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: bridge_conf[CONF_HOST]}, + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a bridge from a config entry.""" + LOGGER.debug("__init async_setup_entry %s", entry.data) + host = entry.data[CONF_HOST] + config = hass.data[DOMAIN][DATA_CONFIGS].get(host) + + if config is None: + LOGGER.error("__init async_setup_entry empty config for host %s", host) + return False + + bridge = DynaliteBridge(hass, entry) + + if not await bridge.async_setup(): + LOGGER.error("bridge.async_setup failed") + return False + hass.data[DOMAIN][entry.entry_id] = bridge + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + LOGGER.error("async_unload_entry %s", entry.data) + bridge = hass.data[DOMAIN].pop(entry.entry_id) + return await bridge.async_reset() diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py new file mode 100755 index 00000000000..1bf86001cc5 --- /dev/null +++ b/homeassistant/components/dynalite/bridge.py @@ -0,0 +1,118 @@ +"""Code to handle a Dynalite bridge.""" + +from dynalite_devices_lib import DynaliteDevices +from dynalite_lib import CONF_ALL + +from homeassistant.const import CONF_HOST +from homeassistant.core import callback + +from .const import DATA_CONFIGS, DOMAIN, LOGGER +from .light import DynaliteLight + + +class BridgeError(Exception): + """Class to throw exceptions from DynaliteBridge.""" + + def __init__(self, message): + """Initialize the exception.""" + super().__init__() + self.message = message + + +class DynaliteBridge: + """Manages a single Dynalite bridge.""" + + def __init__(self, hass, config_entry): + """Initialize the system based on host parameter.""" + self.config_entry = config_entry + self.hass = hass + self.area = {} + self.async_add_entities = None + self.waiting_entities = [] + self.all_entities = {} + self.config = None + self.host = config_entry.data[CONF_HOST] + if self.host not in hass.data[DOMAIN][DATA_CONFIGS]: + LOGGER.info("invalid host - %s", self.host) + raise BridgeError(f"invalid host - {self.host}") + self.config = hass.data[DOMAIN][DATA_CONFIGS][self.host] + # Configure the dynalite devices + self.dynalite_devices = DynaliteDevices( + config=self.config, + newDeviceFunc=self.add_devices, + updateDeviceFunc=self.update_device, + ) + + async def async_setup(self, tries=0): + """Set up a Dynalite bridge.""" + # Configure the dynalite devices + await self.dynalite_devices.async_setup() + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "light" + ) + ) + + return True + + @callback + def add_devices(self, devices): + """Call when devices should be added to home assistant.""" + added_entities = [] + + for device in devices: + if device.category == "light": + entity = DynaliteLight(device, self) + else: + LOGGER.debug("Illegal device category %s", device.category) + continue + added_entities.append(entity) + self.all_entities[entity.unique_id] = entity + + if added_entities: + self.add_entities_when_registered(added_entities) + + @callback + def update_device(self, device): + """Call when a device or all devices should be updated.""" + if device == CONF_ALL: + # This is used to signal connection or disconnection, so all devices may become available or not. + if self.dynalite_devices.available: + LOGGER.info("Connected to dynalite host") + else: + LOGGER.info("Disconnected from dynalite host") + for uid in self.all_entities: + self.all_entities[uid].try_schedule_ha() + else: + uid = device.unique_id + if uid in self.all_entities: + self.all_entities[uid].try_schedule_ha() + + @callback + def register_add_entities(self, async_add_entities): + """Add an async_add_entities for a category.""" + self.async_add_entities = async_add_entities + if self.waiting_entities: + self.async_add_entities(self.waiting_entities) + + def add_entities_when_registered(self, entities): + """Add the entities to HA if async_add_entities was registered, otherwise queue until it is.""" + if not entities: + return + if self.async_add_entities: + self.async_add_entities(entities) + else: # handle it later when it is registered + self.waiting_entities.extend(entities) + + async def async_reset(self): + """Reset this bridge to default state. + + Will cancel any scheduled setup retry and will unload + the config entry. + """ + result = await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, "light" + ) + # None and True are OK + return result diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py new file mode 100755 index 00000000000..9aaaee00717 --- /dev/null +++ b/homeassistant/components/dynalite/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow to configure Dynalite hub.""" +import asyncio + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.core import callback + +from .const import DOMAIN, LOGGER + + +@callback +def configured_hosts(hass): + """Return a set of the configured hosts.""" + return set( + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Dynalite config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + + def __init__(self): + """Initialize the Dynalite flow.""" + self.host = None + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry.""" + LOGGER.debug("async_step_import - %s", import_info) + host = self.context[CONF_HOST] = import_info[CONF_HOST] + return await self._entry_from_bridge(host) + + async def _entry_from_bridge(self, host): + """Return a config entry from an initialized bridge.""" + LOGGER.debug("entry_from_bridge - %s", host) + # Remove all other entries of hubs with same ID or host + + same_hub_entries = [ + entry.entry_id + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.data[CONF_HOST] == host + ] + + LOGGER.debug("entry_from_bridge same_hub - %s", same_hub_entries) + + if same_hub_entries: + await asyncio.wait( + [ + self.hass.config_entries.async_remove(entry_id) + for entry_id in same_hub_entries + ] + ) + + return self.async_create_entry(title=host, data={CONF_HOST: host}) diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py new file mode 100755 index 00000000000..f433214913a --- /dev/null +++ b/homeassistant/components/dynalite/const.py @@ -0,0 +1,11 @@ +"""Constants for the Dynalite component.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "dynalite" +DATA_CONFIGS = "dynalite_configs" + +CONF_BRIDGES = "bridges" + +DEFAULT_NAME = "dynalite" +DEFAULT_PORT = 12345 diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py new file mode 100755 index 00000000000..d3263941f9f --- /dev/null +++ b/homeassistant/components/dynalite/light.py @@ -0,0 +1,84 @@ +"""Support for Dynalite channels as lights.""" +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.core import callback + +from .const import DOMAIN, LOGGER + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Record the async_add_entities function to add them later when received from Dynalite.""" + LOGGER.debug("async_setup_entry light entry = %s", config_entry.data) + bridge = hass.data[DOMAIN][config_entry.entry_id] + bridge.register_add_entities(async_add_entities) + + +class DynaliteLight(Light): + """Representation of a Dynalite Channel as a Home Assistant Light.""" + + def __init__(self, device, bridge): + """Initialize the base class.""" + self._device = device + self._bridge = bridge + + @property + def device(self): + """Return the underlying device - mostly for testing.""" + return self._device + + @property + def name(self): + """Return the name of the entity.""" + return self._device.name + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._device.unique_id + + @property + def available(self): + """Return if entity is available.""" + return self._device.available + + @property + def hidden(self): + """Return true if this entity should be hidden from UI.""" + return self._device.hidden + + async def async_update(self): + """Update the entity.""" + return + + @property + def device_info(self): + """Device info for this entity.""" + return self._device.device_info + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._device.brightness + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + await self._device.async_turn_on(**kwargs) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._device.async_turn_off(**kwargs) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @callback + def try_schedule_ha(self): + """Schedule update HA state if configured.""" + if self.hass: + self.schedule_update_ha_state() diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json new file mode 100755 index 00000000000..4df580c16a2 --- /dev/null +++ b/homeassistant/components/dynalite/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "dynalite", + "name": "Philips Dynalite", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/dynalite", + "dependencies": [], + "codeowners": ["@ziv1234"], + "requirements": ["dynalite_devices==0.1.17"] +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c77a8de9388..8b6c0e77585 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -20,6 +20,7 @@ FLOWS = [ "daikin", "deconz", "dialogflow", + "dynalite", "ecobee", "elgato", "emulated_roku", diff --git a/requirements_all.txt b/requirements_all.txt index acfb089c203..e22b3ccf0f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,6 +455,9 @@ dsmr_parser==0.18 # homeassistant.components.dweet dweepy==0.3.0 +# homeassistant.components.dynalite +dynalite_devices==0.1.17 + # homeassistant.components.rainforest_eagle eagle200_reader==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af22deb6b6c..8aa12234896 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,6 +167,9 @@ distro==1.4.0 # homeassistant.components.dsmr dsmr_parser==0.18 +# homeassistant.components.dynalite +dynalite_devices==0.1.17 + # homeassistant.components.ee_brightbox eebrightbox==0.0.4 diff --git a/tests/components/dynalite/__init__.py b/tests/components/dynalite/__init__.py new file mode 100755 index 00000000000..f97770cbac9 --- /dev/null +++ b/tests/components/dynalite/__init__.py @@ -0,0 +1 @@ +"""Tests for the Dynalite component.""" diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py new file mode 100755 index 00000000000..c0aa2b3c143 --- /dev/null +++ b/tests/components/dynalite/test_bridge.py @@ -0,0 +1,136 @@ +"""Test Dynalite bridge.""" +from unittest.mock import Mock, call, patch + +from dynalite_lib import CONF_ALL +import pytest + +from homeassistant.components.dynalite import DATA_CONFIGS, DOMAIN +from homeassistant.components.dynalite.bridge import BridgeError, DynaliteBridge + +from tests.common import mock_coro + + +async def test_bridge_setup(): + """Test a successful setup.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + + with patch.object( + dyn_bridge.dynalite_devices, "async_setup", return_value=mock_coro(True) + ): + assert await dyn_bridge.async_setup() is True + + forward_entries = set( + c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls + ) + hass.config_entries.async_forward_entry_setup.assert_called_once() + assert forward_entries == set(["light"]) + + +async def test_invalid_host(): + """Test without host in hass.data.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {}}} + + dyn_bridge = None + with pytest.raises(BridgeError): + dyn_bridge = DynaliteBridge(hass, entry) + assert dyn_bridge is None + + +async def test_add_devices_then_register(): + """Test that add_devices work.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + + device1 = Mock() + device1.category = "light" + device2 = Mock() + device2.category = "switch" + dyn_bridge.add_devices([device1, device2]) + reg_func = Mock() + dyn_bridge.register_add_entities(reg_func) + reg_func.assert_called_once() + assert reg_func.mock_calls[0][1][0][0].device is device1 + + +async def test_register_then_add_devices(): + """Test that add_devices work after register_add_entities.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + + device1 = Mock() + device1.category = "light" + device2 = Mock() + device2.category = "switch" + reg_func = Mock() + dyn_bridge.register_add_entities(reg_func) + dyn_bridge.add_devices([device1, device2]) + reg_func.assert_called_once() + assert reg_func.mock_calls[0][1][0][0].device is device1 + + +async def test_update_device(): + """Test the update_device callback.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + with patch.object(dyn_bridge, "dynalite_devices") as devices_mock: + # Single device update + device1 = Mock() + device1.unique_id = "testing1" + device2 = Mock() + device2.unique_id = "testing2" + dyn_bridge.all_entities = { + device1.unique_id: device1, + device2.unique_id: device2, + } + dyn_bridge.update_device(device1) + device1.try_schedule_ha.assert_called_once() + device2.try_schedule_ha.assert_not_called() + # connected to network - all devices update + devices_mock.available = True + dyn_bridge.update_device(CONF_ALL) + assert device1.try_schedule_ha.call_count == 2 + device2.try_schedule_ha.assert_called_once() + # disconnected from network - all devices update + devices_mock.available = False + dyn_bridge.update_device(CONF_ALL) + assert device1.try_schedule_ha.call_count == 3 + assert device2.try_schedule_ha.call_count == 2 + + +async def test_async_reset(): + """Test async_reset.""" + hass = Mock() + hass.config_entries.async_forward_entry_unload = Mock( + return_value=mock_coro(Mock()) + ) + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + await dyn_bridge.async_reset() + hass.config_entries.async_forward_entry_unload.assert_called_once() + assert hass.config_entries.async_forward_entry_unload.mock_calls[0] == call( + entry, "light" + ) diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py new file mode 100755 index 00000000000..1cf82143f1b --- /dev/null +++ b/tests/components/dynalite/test_config_flow.py @@ -0,0 +1,36 @@ +"""Test Dynalite config flow.""" +from unittest.mock import Mock, call, patch + +from homeassistant.components.dynalite import config_flow + +from tests.common import mock_coro + + +async def test_step_import(): + """Test a successful setup.""" + flow_handler = config_flow.DynaliteFlowHandler() + with patch.object(flow_handler, "context", create=True): + with patch.object(flow_handler, "hass", create=True) as mock_hass: + with patch.object( + flow_handler, "async_create_entry", create=True + ) as mock_create: + host = "1.2.3.4" + entry1 = Mock() + entry1.data = {"host": host} + entry2 = Mock() + entry2.data = {"host": "5.5"} + mock_hass.config_entries.async_entries = Mock( + return_value=[entry1, entry2] + ) + mock_hass.config_entries.async_remove = Mock( + return_value=mock_coro(Mock()) + ) + await flow_handler.async_step_import({"host": "1.2.3.4"}) + mock_hass.config_entries.async_remove.assert_called_once() + assert mock_hass.config_entries.async_remove.mock_calls[0] == call( + entry1.entry_id + ) + mock_create.assert_called_once() + assert mock_create.mock_calls[0] == call( + title=host, data={"host": host} + ) diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py new file mode 100755 index 00000000000..beb96a5e78f --- /dev/null +++ b/tests/components/dynalite/test_init.py @@ -0,0 +1,74 @@ +"""Test Dynalite __init__.""" +from unittest.mock import Mock, call, patch + +from homeassistant.components.dynalite import DATA_CONFIGS, DOMAIN, LOGGER +from homeassistant.components.dynalite.__init__ import ( + async_setup, + async_setup_entry, + async_unload_entry, +) + +from tests.common import mock_coro + + +async def test_async_setup(): + """Test a successful setup.""" + new_host = "1.2.3.4" + old_host = "5.6.7.8" + hass = Mock() + hass.data = {} + config = {DOMAIN: {"bridges": [{"host": old_host}, {"host": new_host}]}} + mock_conf_host = Mock(return_value=[old_host]) + with patch( + "homeassistant.components.dynalite.__init__.configured_hosts", mock_conf_host + ): + await async_setup(hass, config) + mock_conf_host.assert_called_once() + assert mock_conf_host.mock_calls[0] == call(hass) + assert hass.data[DOMAIN][DATA_CONFIGS] == { + new_host: {"host": new_host}, + old_host: {"host": old_host}, + } + hass.async_create_task.assert_called_once() + + +async def test_async_setup_entry(): + """Test setup of an entry.""" + + def async_mock(mock): + """Return the return value of a mock from async.""" + + async def async_func(*args, **kwargs): + return mock() + + return async_func + + host = "1.2.3.4" + hass = Mock() + entry = Mock() + entry.data = {"host": host} + hass.data = {} + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CONFIGS] = {host: {}} + mock_async_setup = Mock(return_value=True) + with patch( + "homeassistant.components.dynalite.__init__.DynaliteBridge.async_setup", + async_mock(mock_async_setup), + ): + assert await async_setup_entry(hass, entry) + mock_async_setup.assert_called_once() + + +async def test_async_unload_entry(): + """Test unloading of an entry.""" + hass = Mock() + mock_bridge = Mock() + mock_bridge.async_reset.return_value = mock_coro(True) + entry = Mock() + hass.data = {} + hass.data[DOMAIN] = {} + hass.data[DOMAIN][entry.entry_id] = mock_bridge + await async_unload_entry(hass, entry) + LOGGER.error("XXX calls=%s", mock_bridge.mock_calls) + mock_bridge.async_reset.assert_called_once() + assert mock_bridge.mock_calls[0] == call.async_reset() diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py new file mode 100755 index 00000000000..cfc9d42d0e4 --- /dev/null +++ b/tests/components/dynalite/test_light.py @@ -0,0 +1,44 @@ +"""Test Dynalite light.""" +from unittest.mock import Mock, call, patch + +from homeassistant.components.dynalite import DOMAIN +from homeassistant.components.dynalite.light import DynaliteLight, async_setup_entry + +from tests.common import mock_coro + + +async def test_light_setup(): + """Test a successful setup.""" + hass = Mock() + entry = Mock() + async_add = Mock() + bridge = Mock() + hass.data = {DOMAIN: {entry.entry_id: bridge}} + await async_setup_entry(hass, entry, async_add) + bridge.register_add_entities.assert_called_once() + assert bridge.register_add_entities.mock_calls[0] == call(async_add) + + +async def test_light(): + """Test the light entity.""" + device = Mock() + device.async_turn_on = Mock(return_value=mock_coro(Mock())) + device.async_turn_off = Mock(return_value=mock_coro(Mock())) + bridge = Mock() + dyn_light = DynaliteLight(device, bridge) + assert dyn_light.name is device.name + assert dyn_light.unique_id is device.unique_id + assert dyn_light.available is device.available + assert dyn_light.hidden is device.hidden + await dyn_light.async_update() # does nothing + assert dyn_light.device_info is device.device_info + assert dyn_light.brightness is device.brightness + assert dyn_light.is_on is device.is_on + await dyn_light.async_turn_on(aaa="bbb") + assert device.async_turn_on.mock_calls[0] == call(aaa="bbb") + await dyn_light.async_turn_off(ccc="ddd") + assert device.async_turn_off.mock_calls[0] == call(ccc="ddd") + with patch.object(dyn_light, "hass"): + with patch.object(dyn_light, "schedule_update_ha_state") as update_ha: + dyn_light.try_schedule_ha() + update_ha.assert_called_once() From 94da129ef86142bf1b10982c45cb1bee960ccff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Mon, 10 Feb 2020 22:56:40 +0100 Subject: [PATCH 185/378] Extend Modbus binary sensor to support discrete inputs (#30004) * Extend Modbus binary sensor to support discrete inputs * Add backward compatibility for Modbus binary sensor --- homeassistant/components/modbus/__init__.py | 6 ++ .../components/modbus/binary_sensor.py | 83 ++++++++++++------- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 823703ac4c9..db525e23935 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -213,6 +213,12 @@ class ModbusHub: kwargs = {"unit": unit} if unit else {} return self._client.read_coils(address, count, **kwargs) + def read_discrete_inputs(self, unit, address, count): + """Read discrete inputs.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_discrete_inputs(address, count, **kwargs) + def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 9a431d24b0c..6959f3b47b8 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for Modbus Coil sensors.""" +"""Support for Modbus Coil and Discrete Input sensors.""" import logging from typing import Optional @@ -16,52 +16,72 @@ from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN _LOGGER = logging.getLogger(__name__) -CONF_COIL = "coil" -CONF_COILS = "coils" +CONF_DEPRECATED_COIL = "coil" +CONF_DEPRECATED_COILS = "coils" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COILS): [ - { - vol.Required(CONF_COIL): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int, - } - ] - } +CONF_INPUTS = "inputs" +CONF_INPUT_TYPE = "input_type" +CONF_ADDRESS = "address" + +INPUT_TYPE_COIL = "coil" +INPUT_TYPE_DISCRETE = "discrete_input" + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_INPUTS): [ + vol.All( + cv.deprecated(CONF_DEPRECATED_COIL, CONF_ADDRESS), + vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Optional(CONF_SLAVE): cv.positive_int, + vol.Optional( + CONF_INPUT_TYPE, default=INPUT_TYPE_COIL + ): vol.In([INPUT_TYPE_COIL, INPUT_TYPE_DISCRETE]), + } + ), + ) + ] + } + ), ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] - for coil in config.get(CONF_COILS): - hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)] + for entry in config.get(CONF_INPUTS): + hub = hass.data[MODBUS_DOMAIN][entry.get(CONF_HUB)] sensors.append( - ModbusCoilSensor( + ModbusBinarySensor( hub, - coil.get(CONF_NAME), - coil.get(CONF_SLAVE), - coil.get(CONF_COIL), - coil.get(CONF_DEVICE_CLASS), + entry.get(CONF_NAME), + entry.get(CONF_SLAVE), + entry.get(CONF_ADDRESS), + entry.get(CONF_DEVICE_CLASS), + entry.get(CONF_INPUT_TYPE), ) ) add_entities(sensors) -class ModbusCoilSensor(BinarySensorDevice): - """Modbus coil sensor.""" +class ModbusBinarySensor(BinarySensorDevice): + """Modbus binary sensor.""" - def __init__(self, hub, name, slave, coil, device_class): - """Initialize the Modbus coil sensor.""" + def __init__(self, hub, name, slave, address, device_class, input_type): + """Initialize the Modbus binary sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None - self._coil = int(coil) + self._address = int(address) self._device_class = device_class + self._input_type = input_type self._value = None @property @@ -81,13 +101,16 @@ class ModbusCoilSensor(BinarySensorDevice): def update(self): """Update the state of the sensor.""" - result = self._hub.read_coils(self._slave, self._coil, 1) + if self._input_type == INPUT_TYPE_COIL: + result = self._hub.read_coils(self._slave, self._address, 1) + else: + result = self._hub.read_discrete_inputs(self._slave, self._address, 1) try: self._value = result.bits[0] except AttributeError: _LOGGER.error( - "No response from hub %s, slave %s, coil %s", + "No response from hub %s, slave %s, address %s", self._hub.name, self._slave, - self._coil, + self._address, ) From 454e63b69ee3d74d6d8d5324395794642e35c988 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 10 Feb 2020 14:43:39 -0800 Subject: [PATCH 186/378] Fix Evohome checking override duration (#31697) --- homeassistant/components/evohome/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index b7f6e965a8f..aece0f0ec0d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -175,7 +175,7 @@ class EvoZone(EvoChild, EvoClimateDevice): if ATTR_DURATION_UNTIL in data: duration = data[ATTR_DURATION_UNTIL] - if duration == 0: + if duration.total_seconds() == 0: await self._update_schedule() until = parse_datetime(str(self.setpoints.get("next_sp_from"))) else: From 284fd46ea81f96eb880bd03967655668d2fff4e3 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 10 Feb 2020 17:54:52 -0500 Subject: [PATCH 187/378] For vizio integration, set unique ID early to prevent multiple zeroconf discovery items for the same device to appear (#31686) * set unique ID early to prevent multiple zeroconf discovery items for the same device to appear * add test --- homeassistant/components/vizio/config_flow.py | 5 ++++ tests/components/vizio/test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 04f70da4a8c..4fba0f06165 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -205,6 +205,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> Dict[str, Any]: """Handle zeroconf discovery.""" + # Set unique ID early to prevent device from getting rediscovered multiple times + await self.async_set_unique_id( + unique_id=discovery_info[CONF_HOST].split(":")[0], raise_on_progress=True + ) + discovery_info[ CONF_HOST ] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 069c00bf6b2..5a24e2d1d69 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -405,3 +405,29 @@ async def test_zeroconf_flow_already_configured( # Flow should abort because device is already setup assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" + + +async def test_zeroconf_dupe_fail( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, + vizio_guess_device_type: pytest.fixture, +) -> None: + """Test zeroconf config flow when device gets discovered multiple times.""" + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + + # Form should always show even if all required properties are discovered + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + + # Flow should abort because device is already setup + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" From d55846c33a7c226d7284e384b66efe48c4ca8b02 Mon Sep 17 00:00:00 2001 From: SoftXperience Date: Mon, 10 Feb 2020 23:55:17 +0100 Subject: [PATCH 188/378] =?UTF-8?q?Use=20latest=20version=20of=20python-pu?= =?UTF-8?q?shover=20(forked)=20to=20fix=20issue=20with=20diff=E2=80=A6=20(?= =?UTF-8?q?#31647)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use latest version of python-pushover (forked) to fix issue with different API tokens. (https://community.home-assistant.io/t/different-applications-in-pushover/6985) * Rewrite pushover notify to use pushover_complete library * Remove possibility to attach urls to notifications * Fix comment --- .../components/pushover/manifest.json | 2 +- homeassistant/components/pushover/notify.py | 112 +++++++++--------- requirements_all.txt | 6 +- 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 3428e429b8c..9bdd1bb53f9 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -2,7 +2,7 @@ "domain": "pushover", "name": "Pushover", "documentation": "https://www.home-assistant.io/integrations/pushover", - "requirements": ["python-pushover==0.4"], + "requirements": ["pushover_complete==1.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 1930ff66f2e..bc44cbeddb7 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,8 +1,7 @@ """Pushover platform for notify component.""" import logging -from pushover import Client, InitError, RequestError -import requests +from pushover_complete import PushoverAPI import voluptuous as vol from homeassistant.components.notify import ( @@ -19,6 +18,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) ATTR_ATTACHMENT = "attachment" +ATTR_URL = "url" +ATTR_URL_TITLE = "url_title" +ATTR_PRIORITY = "priority" +ATTR_RETRY = "retry" +ATTR_SOUND = "sound" +ATTR_HTML = "html" +ATTR_CALLBACK_URL = "callback_url" +ATTR_EXPIRE = "expire" +ATTR_TIMESTAMP = "timestamp" CONF_USER_KEY = "user_key" @@ -29,13 +37,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" - try: - return PushoverNotificationService( - hass, config[CONF_USER_KEY], config[CONF_API_KEY] - ) - except InitError: - _LOGGER.error("Wrong API key supplied") - return None + return PushoverNotificationService( + hass, config[CONF_USER_KEY], config[CONF_API_KEY] + ) class PushoverNotificationService(BaseNotificationService): @@ -46,54 +50,42 @@ class PushoverNotificationService(BaseNotificationService): self._hass = hass self._user_key = user_key self._api_token = api_token - self.pushover = Client(self._user_key, api_token=self._api_token) + self.pushover = PushoverAPI(self._api_token) def send_message(self, message="", **kwargs): """Send a message to a user.""" - # Make a copy and use empty dict if necessary + + # Extract params from data dict + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = dict(kwargs.get(ATTR_DATA) or {}) + url = data.get(ATTR_URL, None) + url_title = data.get(ATTR_URL_TITLE, None) + priority = data.get(ATTR_PRIORITY, None) + retry = data.get(ATTR_PRIORITY, None) + expire = data.get(ATTR_EXPIRE, None) + callback_url = data.get(ATTR_CALLBACK_URL, None) + timestamp = data.get(ATTR_TIMESTAMP, None) + sound = data.get(ATTR_SOUND, None) + html = 1 if data.get(ATTR_HTML, False) else 0 - data["title"] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - # Check for attachment. - if ATTR_ATTACHMENT in data: - # If attachment is a URL, use requests to open it as a stream. - if data[ATTR_ATTACHMENT].startswith("http"): + image = data.get(ATTR_ATTACHMENT, None) + # Check for attachment + if image is not None: + # Only allow attachments from whitelisted paths, check valid path + if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): + # try to open it as a normal file. try: - response = requests.get( - data[ATTR_ATTACHMENT], stream=True, timeout=5 - ) - if response.status_code == 200: - # Replace the attachment identifier with file object. - data[ATTR_ATTACHMENT] = response.content - else: - _LOGGER.error( - "Failed to download image %s, response code: %d", - data[ATTR_ATTACHMENT], - response.status_code, - ) - # Remove attachment key to send without attachment. - del data[ATTR_ATTACHMENT] - except requests.exceptions.RequestException as ex_val: + file_handle = open(data[ATTR_ATTACHMENT], "rb") + # Replace the attachment identifier with file object. + image = file_handle + except OSError as ex_val: _LOGGER.error(ex_val) - # Remove attachment key to try sending without attachment - del data[ATTR_ATTACHMENT] - else: - # Not a URL, check valid path first - if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): - # try to open it as a normal file. - try: - file_handle = open(data[ATTR_ATTACHMENT], "rb") - # Replace the attachment identifier with file object. - data[ATTR_ATTACHMENT] = file_handle - except OSError as ex_val: - _LOGGER.error(ex_val) - # Remove attachment key to send without attachment. - del data[ATTR_ATTACHMENT] - else: - _LOGGER.error("Path is not whitelisted") # Remove attachment key to send without attachment. - del data[ATTR_ATTACHMENT] + image = None + else: + _LOGGER.error("Path is not whitelisted") + # Remove attachment key to send without attachment. + image = None targets = kwargs.get(ATTR_TARGET) @@ -101,12 +93,22 @@ class PushoverNotificationService(BaseNotificationService): targets = [targets] for target in targets: - if target is not None: - data["device"] = target - try: - self.pushover.send_message(message, **data) + self.pushover.send_message( + self._user_key, + message, + target, + title, + url, + url_title, + image, + priority, + retry, + expire, + callback_url, + timestamp, + sound, + html, + ) except ValueError as val_err: _LOGGER.error(val_err) - except RequestError: - _LOGGER.exception("Could not send pushover notification") diff --git a/requirements_all.txt b/requirements_all.txt index e22b3ccf0f4..22732005d35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1067,6 +1067,9 @@ pushbullet.py==0.11.0 # homeassistant.components.pushetta pushetta==1.0.15 +# homeassistant.components.pushover +pushover_complete==1.1.1 + # homeassistant.components.rpi_gpio_pwm pwmled==1.4.1 @@ -1611,9 +1614,6 @@ python-nest==4.1.0 # homeassistant.components.nmap_tracker python-nmap==0.6.1 -# homeassistant.components.pushover -python-pushover==0.4 - # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 From c66106ee98677768c9bcd06245967a6f486bc0a5 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 11 Feb 2020 01:02:14 +0200 Subject: [PATCH 189/378] Add Glances sensors dynamically (#28639) * Add temp_sensors to glances dynamically * unsub from updates when sensor is disabled * dynamicall add sensors * change "device_name" to "mnt_point" * remove unnecessary logging * update sensor.py * update test_config_flow.py * remove commented code --- homeassistant/components/glances/const.py | 38 +++--- homeassistant/components/glances/sensor.py | 131 +++++++++++++------ tests/components/glances/test_config_flow.py | 69 ++++++---- 3 files changed, 152 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index e47586ea245..b7f5a2d642b 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -14,23 +14,23 @@ DATA_UPDATED = "glances_data_updated" SUPPORTED_VERSIONS = [2, 3] SENSOR_TYPES = { - "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], - "disk_use": ["Disk used", "GiB", "mdi:harddisk"], - "disk_free": ["Disk free", "GiB", "mdi:harddisk"], - "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], - "memory_use": ["RAM used", "MiB", "mdi:memory"], - "memory_free": ["RAM free", "MiB", "mdi:memory"], - "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], - "swap_use": ["Swap used", "GiB", "mdi:memory"], - "swap_free": ["Swap free", "GiB", "mdi:memory"], - "processor_load": ["CPU load", "15 min", "mdi:memory"], - "process_running": ["Running", "Count", "mdi:memory"], - "process_total": ["Total", "Count", "mdi:memory"], - "process_thread": ["Thread", "Count", "mdi:memory"], - "process_sleeping": ["Sleeping", "Count", "mdi:memory"], - "cpu_use_percent": ["CPU used", "%", "mdi:memory"], - "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], - "docker_active": ["Containers active", "", "mdi:docker"], - "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], - "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], + "disk_use_percent": ["fs", "used percent", "%", "mdi:harddisk"], + "disk_use": ["fs", "used", "GiB", "mdi:harddisk"], + "disk_free": ["fs", "free", "GiB", "mdi:harddisk"], + "memory_use_percent": ["mem", "RAM used percent", "%", "mdi:memory"], + "memory_use": ["mem", "RAM used", "MiB", "mdi:memory"], + "memory_free": ["mem", "RAM free", "MiB", "mdi:memory"], + "swap_use_percent": ["memswap", "Swap used percent", "%", "mdi:memory"], + "swap_use": ["memswap", "Swap used", "GiB", "mdi:memory"], + "swap_free": ["memswap", "Swap free", "GiB", "mdi:memory"], + "processor_load": ["load", "CPU load", "15 min", "mdi:memory"], + "process_running": ["processcount", "Running", "Count", "mdi:memory"], + "process_total": ["processcount", "Total", "Count", "mdi:memory"], + "process_thread": ["processcount", "Thread", "Count", "mdi:memory"], + "process_sleeping": ["processcount", "Sleeping", "Count", "mdi:memory"], + "cpu_use_percent": ["cpu", "CPU used", "%", "mdi:memory"], + "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"], + "docker_active": ["docker", "Containers active", "", "mdi:docker"], + "docker_cpu_use": ["docker", "Containers CPU used", "%", "mdi:docker"], + "docker_memory_use": ["docker", "Containers RAM used", "MiB", "mdi:docker"], } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 968081cfc43..f701dfdb741 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -14,13 +14,51 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Glances sensors.""" - glances_data = hass.data[DOMAIN][config_entry.entry_id] + client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] - for sensor_type in SENSOR_TYPES: - dev.append( - GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type) - ) + + for sensor_type, sensor_details in SENSOR_TYPES.items(): + if not sensor_details[0] in client.api.data: + continue + if sensor_details[0] in client.api.data: + if sensor_details[0] == "fs": + # fs will provide a list of disks attached + for disk in client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + disk["mnt_point"], + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) + elif sensor_details[0] == "sensors": + # sensors will provide temp for different devices + for sensor in client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + sensor["label"], + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) + elif client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + "", + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) async_add_entities(dev, True) @@ -28,19 +66,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class GlancesSensor(Entity): """Implementation of a Glances sensor.""" - def __init__(self, glances_data, name, sensor_name, sensor_type): + def __init__( + self, + glances_data, + name, + sensor_name_prefix, + sensor_name_suffix, + sensor_type, + sensor_details, + ): """Initialize the sensor.""" self.glances_data = glances_data - self._sensor_name = sensor_name + self._sensor_name_prefix = sensor_name_prefix + self._sensor_name_suffix = sensor_name_suffix self._name = name self.type = sensor_type self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.sensor_details = sensor_details + self.unsub_update = None @property def name(self): """Return the name of the sensor.""" - return f"{self._name} {self._sensor_name}" + return f"{self._name} {self._sensor_name_prefix} {self._sensor_name_suffix}" @property def unique_id(self): @@ -50,12 +98,12 @@ class GlancesSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return self.sensor_details[3] @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit_of_measurement + return self.sensor_details[2] @property def available(self): @@ -74,7 +122,7 @@ class GlancesSensor(Entity): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, DATA_UPDATED, self._schedule_immediate_update ) @@ -82,22 +130,40 @@ class GlancesSensor(Entity): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + async def async_update(self): """Get the latest data from REST API.""" value = self.glances_data.api.data + if value is None: + return if value is not None: - if self.type == "disk_use_percent": - self._state = value["fs"][0]["percent"] - elif self.type == "disk_use": - self._state = round(value["fs"][0]["used"] / 1024 ** 3, 1) - elif self.type == "disk_free": - try: - self._state = round(value["fs"][0]["free"] / 1024 ** 3, 1) - except KeyError: - self._state = round( - (value["fs"][0]["size"] - value["fs"][0]["used"]) / 1024 ** 3, 1 - ) + if self.sensor_details[0] == "fs": + for var in value["fs"]: + if var["mnt_point"] == self._sensor_name_prefix: + disk = var + break + if self.type == "disk_use_percent": + self._state = disk["percent"] + elif self.type == "disk_use": + self._state = round(disk["used"] / 1024 ** 3, 1) + elif self.type == "disk_free": + try: + self._state = round(disk["free"] / 1024 ** 3, 1) + except KeyError: + self._state = round( + (disk["size"] - disk["used"]) / 1024 ** 3, 1, + ) + elif self.type == "sensor_temp": + for sensor in value["sensors"]: + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] + break elif self.type == "memory_use_percent": self._state = value["mem"]["percent"] elif self.type == "memory_use": @@ -126,25 +192,6 @@ class GlancesSensor(Entity): self._state = value["processcount"]["sleeping"] elif self.type == "cpu_use_percent": self._state = value["quicklook"]["cpu"] - elif self.type == "cpu_temp": - for sensor in value["sensors"]: - if sensor["label"] in [ - "amdgpu 1", - "aml_thermal", - "Core 0", - "Core 1", - "CPU Temperature", - "CPU", - "cpu-thermal 1", - "cpu_thermal 1", - "exynos-therm 1", - "Package id 0", - "Physical id 0", - "radeon 1", - "soc-thermal 1", - "soc_thermal 1", - ]: - self._state = sensor["value"] elif self.type == "docker_active": count = 0 try: diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index e5be52e6b33..8734ca0e60d 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import patch from glances_api import Glances -from homeassistant.components.glances import config_flow -from homeassistant.components.glances.const import DOMAIN +from homeassistant import data_entry_flow +from homeassistant.components import glances from homeassistant.const import CONF_SCAN_INTERVAL from tests.common import MockConfigEntry, mock_coro @@ -29,22 +29,22 @@ DEMO_USER_INPUT = { } -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.GlancesFlowHandler() - flow.hass = hass - return flow - - async def test_form(hass): """Test config entry configured successfully.""" - flow = init_config_flow(hass) + + result = await hass.config_entries.flow.async_init( + glances.DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" with patch("glances_api.Glances"), patch.object( Glances, "get_data", return_value=mock_coro() ): - result = await flow.async_step_user(DEMO_USER_INPUT) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) assert result["type"] == "create_entry" assert result["title"] == NAME @@ -53,10 +53,14 @@ async def test_form(hass): async def test_form_cannot_connect(hass): """Test to return error if we cannot connect.""" - flow = init_config_flow(hass) with patch("glances_api.Glances"): - result = await flow.async_step_user(DEMO_USER_INPUT) + result = await hass.config_entries.flow.async_init( + glances.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) assert result["type"] == "form" assert result["errors"] == {"base": "cannot_connect"} @@ -64,11 +68,15 @@ async def test_form_cannot_connect(hass): async def test_form_wrong_version(hass): """Test to check if wrong version is entered.""" - flow = init_config_flow(hass) user_input = DEMO_USER_INPUT.copy() user_input.update(version=1) - result = await flow.async_step_user(user_input) + result = await hass.config_entries.flow.async_init( + glances.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) assert result["type"] == "form" assert result["errors"] == {"version": "wrong_version"} @@ -77,13 +85,16 @@ async def test_form_wrong_version(hass): async def test_form_already_configured(hass): """Test host is already configured.""" entry = MockConfigEntry( - domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} ) entry.add_to_hass(hass) - flow = init_config_flow(hass) - result = await flow.async_step_user(DEMO_USER_INPUT) - + result = await hass.config_entries.flow.async_init( + glances.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -91,12 +102,20 @@ async def test_form_already_configured(hass): async def test_options(hass): """Test options for Glances.""" entry = MockConfigEntry( - domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} ) entry.add_to_hass(hass) - flow = init_config_flow(hass) - options_flow = flow.async_get_options_flow(entry) - result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) - assert result["type"] == "create_entry" - assert result["data"][CONF_SCAN_INTERVAL] == 10 + result = await hass.config_entries.options.async_init(entry.entry_id) + + 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={glances.CONF_SCAN_INTERVAL: 10} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + glances.CONF_SCAN_INTERVAL: 10, + } From 2db6246244441025facc9f27f5ed1a591cf21a78 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 11 Feb 2020 00:31:53 +0000 Subject: [PATCH 190/378] [ci skip] Translation update --- .../components/gdacs/.translations/no.json | 9 +++- .../components/melcloud/.translations/da.json | 23 ++++++++++ .../components/melcloud/.translations/en.json | 42 +++++++++---------- .../minecraft_server/.translations/no.json | 9 +++- .../.translations/zh-Hant.json | 24 +++++++++++ 5 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/melcloud/.translations/da.json create mode 100644 homeassistant/components/minecraft_server/.translations/zh-Hant.json diff --git a/homeassistant/components/gdacs/.translations/no.json b/homeassistant/components/gdacs/.translations/no.json index c06ad6378b5..54b3ca68451 100644 --- a/homeassistant/components/gdacs/.translations/no.json +++ b/homeassistant/components/gdacs/.translations/no.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert." + }, "step": { "user": { "data": { "radius": "Radius" - } + }, + "title": "Fyll ut filterdetaljene." } - } + }, + "title": "Globalt katastrofevarslings- og koordineringssystem (GDACS)" } } \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/da.json b/homeassistant/components/melcloud/.translations/da.json new file mode 100644 index 00000000000..6901ed22934 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud-integration er allerede konfigureret for denne e-mail. Adgangstoken er blevet opdateret." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse. Pr\u00f8v igen", + "invalid_auth": "Ugyldig godkendelse", + "unknown": "Uventet fejl" + }, + "step": { + "user": { + "data": { + "password": "MELCloud-adgangskode.", + "username": "E-mail, der bruges til at logge ind p\u00e5 MELCloud." + }, + "description": "Opret forbindelse ved hj\u00e6lp af din MELCloud-konto.", + "title": "Opret forbindelse til MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/en.json b/homeassistant/components/melcloud/.translations/en.json index 477ca7eb5e2..48682f617a3 100644 --- a/homeassistant/components/melcloud/.translations/en.json +++ b/homeassistant/components/melcloud/.translations/en.json @@ -1,23 +1,23 @@ { - "config": { - "title": "MELCloud", - "step": { - "user": { - "title": "Connect to MELCloud", - "description": "Connect using your MELCloud account.", - "data": { - "username": "Email used to login to MELCloud.", - "password": "MELCloud password." - } - } - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + "config": { + "abort": { + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "MELCloud password.", + "username": "Email used to login to MELCloud." + }, + "description": "Connect using your MELCloud account.", + "title": "Connect to MELCloud" + } + }, + "title": "MELCloud" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json index fad919c0473..f7be289d48c 100644 --- a/homeassistant/components/minecraft_server/.translations/no.json +++ b/homeassistant/components/minecraft_server/.translations/no.json @@ -4,6 +4,8 @@ "already_configured": "Verten er allerede konfigurert." }, "error": { + "cannot_connect": "Kan ikke koble til serveren. Kontroller verten og porten, og pr\u00f8v p\u00e5 nytt. S\u00f8rg ogs\u00e5 for at du kj\u00f8rer minst Minecraft versjon 1.7 p\u00e5 serveren din.", + "invalid_ip": "IP-adressen er ugyldig (MAC-adressen kan ikke fastsl\u00e5s). Vennligst korriger den og pr\u00f8v p\u00e5 nytt.", "invalid_port": "Porten m\u00e5 v\u00e6re i omr\u00e5det 1024 til 65535. Vennligst korriger den og pr\u00f8v p\u00e5 nytt." }, "step": { @@ -12,8 +14,11 @@ "host": "Vert", "name": "Navn", "port": "Port" - } + }, + "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", + "title": "Link din Minecraft Server" } - } + }, + "title": "Minecraft Server" } } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/zh-Hant.json b/homeassistant/components/minecraft_server/.translations/zh-Hant.json new file mode 100644 index 00000000000..c451ad71065 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u5f8c\u518d\u8a66\u4e00\u6b21\u3002\u53e6\u8acb\u78ba\u8a8d\u65bc\u4f3a\u670d\u5668\u4e0a\u57f7\u884c\u6700\u65b0\u7248\u672c Minecraft 1.7 \u7248\u3002", + "invalid_ip": "IP \u4f4d\u5740\u7121\u6548\uff08MAC \u4f4d\u5740\u7121\u6cd5\u78ba\u8a8d\uff09\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_port": "\u901a\u8a0a\u57e0\u7bc4\u570d\u4ecb\u65bc 1024 \u81f3 65535\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Minecraft \u4f3a\u670d\u5668\u4ee5\u9032\u884c\u76e3\u63a7\u3002", + "title": "\u9023\u7d50 Minecraft \u4f3a\u670d\u5668" + } + }, + "title": "Minecraft \u4f3a\u670d\u5668" + } +} \ No newline at end of file From 5a0f21cbe356a29c918975f7969dedf1333cda40 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Feb 2020 16:32:47 -0800 Subject: [PATCH 191/378] Adjust entity slow warning for custom component (#31711) --- homeassistant/helpers/entity.py | 20 ++++++++++++++------ tests/helpers/test_entity.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 250b81bb0fb..92072b22df2 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -365,17 +365,25 @@ class Entity(ABC): if end - start > 0.4 and not self._slow_reported: self._slow_reported = True - url = "https://github.com/home-assistant/home-assistant/issues?q=is%3Aopen+is%3Aissue" - if self.platform: - url += f"+label%3A%22integration%3A+{self.platform.platform_name}%22" + extra = "" + if "custom_components" in type(self).__module__: + extra = "Please report it to the custom component author." + else: + extra = ( + "Please create a bug report at " + "https://github.com/home-assistant/home-assistant/issues?q=is%3Aopen+is%3Aissue" + ) + if self.platform: + extra += ( + f"+label%3A%22integration%3A+{self.platform.platform_name}%22" + ) _LOGGER.warning( - "Updating state for %s (%s) took %.3f seconds. " - "Please create a bug report at %s", + "Updating state for %s (%s) took %.3f seconds. %s", self.entity_id, type(self), end - start, - url, + extra, ) # Overwrite properties that have been set in the config file. diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9977c99904a..6dc194c09d4 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -680,3 +680,24 @@ async def test_warn_slow_write_state(hass, caplog): "https://github.com/home-assistant/home-assistant/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text + + +async def test_warn_slow_write_state_custom_component(hass, caplog): + """Check that we log a warning if reading properties takes too long.""" + + class CustomComponentEntity(entity.Entity): + __module__ = "custom_components.bla.sensor" + + mock_entity = CustomComponentEntity() + mock_entity.hass = hass + mock_entity.entity_id = "comp_test.test_entity" + mock_entity.platform = MagicMock(platform_name="hue") + + with patch("homeassistant.helpers.entity.timer", side_effect=[0, 10]): + mock_entity.async_write_ha_state() + + assert ( + "Updating state for comp_test.test_entity " + "(.CustomComponentEntity'>) " + "took 10.000 seconds. Please report it to the custom component author." + ) in caplog.text From 6fcf5472a5e69d7e007bf614ce6b0f94192b2d5a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Feb 2020 00:03:52 -0800 Subject: [PATCH 192/378] Limit derivative test (#31717) --- tests/components/derivative/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 05ce55223d0..9ffa29571b7 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -131,8 +131,8 @@ async def test_data_moving_average_for_discrete_sensor(hass): # (because the true derivative is 1 °C/min) an error of less than 10%. temperature_values = [] - for temperature in range(60): - temperature_values += [temperature] * 60 + for temperature in range(6): + temperature_values += [temperature] * 6 time_window = 600 times = list(range(len(temperature_values))) From 8a6158116a10d98c7433176b58f53abc2b524b62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Feb 2020 00:07:46 -0800 Subject: [PATCH 193/378] Fix person reload service (#31716) --- homeassistant/components/person/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index dabcc046f7a..a1620c578e3 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -315,7 +315,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): conf = await entity_component.async_prepare_reload(skip_reset=True) if conf is None: return - await yaml_collection.async_load(await filter_yaml_data(hass, conf[DOMAIN])) + await yaml_collection.async_load( + await filter_yaml_data(hass, conf.get(DOMAIN, [])) + ) service.async_register_admin_service( hass, DOMAIN, SERVICE_RELOAD, async_reload_yaml From 3df2cb6b7836bd8a93af5ed69210d64db9f51117 Mon Sep 17 00:00:00 2001 From: Victor Vostrikov <1998617+gorynychzmey@users.noreply.github.com> Date: Tue, 11 Feb 2020 17:46:02 +0100 Subject: [PATCH 194/378] Add support of multiple Tado accounts (#31527) * Added support of multiple Tado accounts Changed geberation of sensor unique id (breaking change) * Fixed lints * Fixed error detecting opened window * Changed gereration of unique id of climate * Removed commented code and added comments --- homeassistant/components/tado/__init__.py | 71 ++++++++++++++--------- homeassistant/components/tado/climate.py | 38 ++++++------ homeassistant/components/tado/const.py | 1 + homeassistant/components/tado/sensor.py | 66 ++++++++++----------- 4 files changed, 93 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index ebf605bdc75..dbc4e87b650 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import Throttle -from .const import CONF_FALLBACK +from .const import CONF_FALLBACK, DATA _LOGGER = logging.getLogger(__name__) @@ -27,12 +27,15 @@ SCAN_INTERVAL = timedelta(seconds=15) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_FALLBACK, default=True): cv.boolean, - } + DOMAIN: vol.All( + cv.ensure_list, + [ + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FALLBACK, default=True): cv.boolean, + } + ], ) }, extra=vol.ALLOW_EXTRA, @@ -41,45 +44,54 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up of the Tado component.""" - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] + acc_list = config[DOMAIN] - tadoconnector = TadoConnector(hass, username, password) - if not tadoconnector.setup(): - return False + api_data_list = [] - hass.data[DOMAIN] = tadoconnector + for acc in acc_list: + username = acc[CONF_USERNAME] + password = acc[CONF_PASSWORD] + fallback = acc[CONF_FALLBACK] - # Do first update - tadoconnector.update() + tadoconnector = TadoConnector(hass, username, password, fallback) + if not tadoconnector.setup(): + continue + + # Do first update + tadoconnector.update() + + api_data_list.append(tadoconnector) + # Poll for updates in the background + hass.helpers.event.track_time_interval( + # we're using here tadoconnector as a parameter of lambda + # to capture actual value instead of closuring of latest value + lambda now, tc=tadoconnector: tc.update(), + SCAN_INTERVAL, + ) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA] = api_data_list # Load components for component in TADO_COMPONENTS: load_platform( - hass, - component, - DOMAIN, - {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]}, - config, + hass, component, DOMAIN, {}, config, ) - # Poll for updates in the background - hass.helpers.event.track_time_interval( - lambda now: tadoconnector.update(), SCAN_INTERVAL - ) - return True class TadoConnector: """An object to store the Tado data.""" - def __init__(self, hass, username, password): + def __init__(self, hass, username, password, fallback): """Initialize Tado Connector.""" self.hass = hass self._username = username self._password = password + self._fallback = fallback + self.device_id = None self.tado = None self.zones = None self.devices = None @@ -88,6 +100,11 @@ class TadoConnector: "device": {}, } + @property + def fallback(self): + """Return fallback flag to Smart Schedule.""" + return self._fallback + def setup(self): """Connect to Tado and fetch the zones.""" try: @@ -101,7 +118,7 @@ class TadoConnector: # Load zones and devices self.zones = self.tado.getZones() self.devices = self.tado.getMe()["homes"] - + self.device_id = self.devices[0]["id"] return True @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 88433db0991..44e35bce787 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -25,7 +25,7 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, @@ -70,21 +70,20 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" - tado = hass.data[DOMAIN] - + api_list = hass.data[DOMAIN][DATA] entities = [] - for zone in tado.zones: - entity = create_climate_entity( - tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK] - ) - if entity: - entities.append(entity) + + for tado in api_list: + for zone in tado.zones: + entity = create_climate_entity(tado, zone["name"], zone["id"]) + if entity: + entities.append(entity) if entities: add_entities(entities, True) -def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): +def create_climate_entity(tado, name: str, zone_id: int): """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) @@ -112,15 +111,7 @@ def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): step = temperatures["celsius"].get("step", PRECISION_TENTHS) entity = TadoClimate( - tado, - name, - zone_id, - zone_type, - min_temp, - max_temp, - step, - ac_support_heat, - fallback, + tado, name, zone_id, zone_type, min_temp, max_temp, step, ac_support_heat, ) return entity @@ -138,7 +129,6 @@ class TadoClimate(ClimateDevice): max_temp, step, ac_support_heat, - fallback, ): """Initialize of Tado climate entity.""" self._tado = tado @@ -146,6 +136,7 @@ class TadoClimate(ClimateDevice): self.zone_name = zone_name self.zone_id = zone_id self.zone_type = zone_type + self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_support_heat = ac_support_heat @@ -162,7 +153,7 @@ class TadoClimate(ClimateDevice): self._step = step self._target_temp = None - if fallback: + if tado.fallback: _LOGGER.debug("Default overlay is set to TADO MODE") # Fallback to Smart Schedule at next Schedule switch self._default_overlay = CONST_OVERLAY_TADO_MODE @@ -199,6 +190,11 @@ class TadoClimate(ClimateDevice): """Return the name of the entity.""" return self.zone_name + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def should_poll(self) -> bool: """Do not poll.""" diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 3c0232c8ba2..8d67e3bf9f8 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -2,6 +2,7 @@ # Configuration CONF_FALLBACK = "fallback" +DATA = "data" # Types TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index a928b61a508..f5f32a6ed1a 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER _LOGGER = logging.getLogger(__name__) @@ -40,26 +40,29 @@ DEVICE_SENSORS = ["tado bridge status"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - tado = hass.data[DOMAIN] + api_list = hass.data[DOMAIN][DATA] - # Create zone sensors entities = [] - for zone in tado.zones: - entities.extend( - [ - create_zone_sensor(tado, zone["name"], zone["id"], variable) - for variable in ZONE_SENSORS.get(zone["type"]) - ] - ) - # Create device sensors - for home in tado.devices: - entities.extend( - [ - create_device_sensor(tado, home["name"], home["id"], variable) - for variable in DEVICE_SENSORS - ] - ) + for tado in api_list: + # Create zone sensors + + for zone in tado.zones: + entities.extend( + [ + create_zone_sensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS.get(zone["type"]) + ] + ) + + # Create device sensors + for home in tado.devices: + entities.extend( + [ + create_device_sensor(tado, home["name"], home["id"], variable) + for variable in DEVICE_SENSORS + ] + ) add_entities(entities, True) @@ -86,7 +89,7 @@ class TadoSensor(Entity): self.zone_variable = zone_variable self.sensor_type = sensor_type - self._unique_id = f"{zone_variable} {zone_id}" + self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}" self._state = None self._state_attributes = None @@ -227,23 +230,16 @@ class TadoSensor(Entity): self._state = data["tadoMode"] elif self.zone_variable == "overlay": - if "overlay" in data and data["overlay"] is not None: - self._state = True - self._state_attributes = { - "termination": data["overlay"]["termination"]["type"] - } - else: - self._state = False - self._state_attributes = {} + self._state = "overlay" in data and data["overlay"] is not None + self._state_attributes = ( + {"termination": data["overlay"]["termination"]["type"]} + if self._state + else {} + ) elif self.zone_variable == "early start": - if "preparation" in data and data["preparation"] is not None: - self._state = True - else: - self._state = False + self._state = "preparation" in data and data["preparation"] is not None elif self.zone_variable == "open window": - if "openWindowDetected" in data: - self._state = data["openWindowDetected"] - else: - self._state = False + self._state = "openWindow" in data and data["openWindow"] is not None + self._state_attributes = data["openWindow"] if self._state else {} From 81b159f4242d88fd1b7915216743f46934c1cf4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Feb 2020 08:50:07 -0800 Subject: [PATCH 195/378] Disable Hue groups for new setups (#31713) --- homeassistant/components/hue/__init__.py | 20 +++++++++++--------- homeassistant/components/hue/config_flow.py | 12 ++++++++++-- homeassistant/components/hue/const.py | 6 ++++++ tests/components/hue/test_config_flow.py | 2 ++ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index ff51fc667e6..dd2905c8783 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -11,22 +11,22 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr from .bridge import HueBridge -from .const import DOMAIN +from .const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_HUE_GROUPS, + DEFAULT_ALLOW_UNREACHABLE, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) CONF_BRIDGES = "bridges" -CONF_ALLOW_UNREACHABLE = "allow_unreachable" -DEFAULT_ALLOW_UNREACHABLE = False - DATA_CONFIGS = "hue_configs" PHUE_CONFIG_FILE = "phue.conf" -CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" -DEFAULT_ALLOW_HUE_GROUPS = True - BRIDGE_CONFIG_SCHEMA = vol.Schema( { # Validate as IP address and then convert back to a string. @@ -112,8 +112,10 @@ async def async_setup_entry( config = hass.data[DATA_CONFIGS].get(host) if config is None: - allow_unreachable = DEFAULT_ALLOW_UNREACHABLE - allow_groups = DEFAULT_ALLOW_HUE_GROUPS + allow_unreachable = entry.data.get( + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE + ) + allow_groups = entry.data.get(CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS) else: allow_unreachable = config[CONF_ALLOW_UNREACHABLE] allow_groups = config[CONF_ALLOW_HUE_GROUPS] diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a46f8816fbb..d214c5509ea 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -14,7 +14,11 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge -from .const import DOMAIN, LOGGER # pylint: disable=unused-import +from .const import ( # pylint: disable=unused-import + CONF_ALLOW_HUE_GROUPS, + DOMAIN, + LOGGER, +) from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" @@ -125,7 +129,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=bridge.config.name, - data={"host": bridge.host, "username": bridge.username}, + data={ + "host": bridge.host, + "username": bridge.username, + CONF_ALLOW_HUE_GROUPS: False, + }, ) except AuthenticationRequired: errors["base"] = "register_failed" diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index d8b33c60048..e2189515482 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -7,3 +7,9 @@ DOMAIN = "hue" # How long to wait to actually do the refresh after requesting it. # We wait some time so if we control multiple lights, we batch requests. REQUEST_REFRESH_DELAY = 0.3 + +CONF_ALLOW_UNREACHABLE = "allow_unreachable" +DEFAULT_ALLOW_UNREACHABLE = False + +CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" +DEFAULT_ALLOW_HUE_GROUPS = True diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 6929d6272df..1ca2eca664e 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -79,6 +79,7 @@ async def test_flow_works(hass): assert result["data"] == { "host": "1.2.3.4", "username": "home-assistant#test-home", + "allow_hue_groups": False, } assert len(mock_bridge.initialize.mock_calls) == 1 @@ -440,6 +441,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): assert result["data"] == { "host": "2.2.2.2", "username": "username-abc", + "allow_hue_groups": False, } entries = hass.config_entries.async_entries("hue") assert len(entries) == 2 From ecd7ec385d8f8db8809ae439e09e9283b0f12d8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Feb 2020 10:57:26 -0600 Subject: [PATCH 196/378] Significantly reduce the number of API calls that the august integration (#31685) * Significantly reduce the number of API calls that the august integration makes. The poll interval for the lock status API is now 15 minutes instead of every 10 seconds because we can use the activity API to see changes in lock state. The interval for the activity API is 10 seconds which allows for the same frequency of state monitoring without all the additional API calls. With four locks, this change results in an ~80% reduction in the number of API calls. The result of the lock and unlock APIs now update the lock state instead of waiting for the next poll. This change also has the added benefit of making the UI appear far more responsive. * Convert to using UTC times --- homeassistant/components/august/__init__.py | 53 +++++++++++++++++++-- homeassistant/components/august/lock.py | 49 ++++++++++++++++++- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index a52df5e361c..a646ee2bad5 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) @@ -42,9 +42,22 @@ DEFAULT_ENTITY_NAMESPACE = "august" # avoid hitting rate limits MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800) -DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) +# Limit locks status check to 900 seconds now that +# we get the state from the lock and unlock api calls +# and the lock and unlock activities are now captured +MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900) + +# Doorbells need to update more frequently than locks +# since we get an image from the doorbell api +MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20) + +# Activity needs to be checked more frequently as the +# doorbell motion and rings are included here MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) + + LOGIN_METHODS = ["phone", "email"] CONFIG_SCHEMA = vol.Schema( @@ -192,6 +205,7 @@ class AugustData: self._house_ids.add(device.house_id) self._doorbell_detail_by_id = {} + self._lock_last_status_update_time_utc_by_id = {} self._lock_status_by_id = {} self._lock_detail_by_id = {} self._door_state_by_id = {} @@ -243,6 +257,7 @@ class AugustData: self._activities_by_id[device_id] = [ a for a in activities if a.device_id == device_id ] + _LOGGER.debug("Completed retrieving device activities") def get_doorbell_detail(self, doorbell_id): @@ -250,7 +265,7 @@ class AugustData: self._update_doorbells() return self._doorbell_detail_by_id.get(doorbell_id) - @Throttle(MIN_TIME_BETWEEN_UPDATES) + @Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES) def _update_doorbells(self): detail_by_id = {} @@ -275,6 +290,17 @@ class AugustData: _LOGGER.debug("Completed retrieving doorbell details") self._doorbell_detail_by_id = detail_by_id + def update_lock_status(self, lock_id, lock_status, update_start_time_utc): + """Set the lock status and last status update time. + + This is used when the lock, unlock apis are called + or newer activity is detected on the activity feed + in order to keep the internal data in sync + """ + self._lock_status_by_id[lock_id] = lock_status + self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc + return True + def get_lock_status(self, lock_id): """Return status if the door is locked or unlocked. @@ -300,13 +326,15 @@ class AugustData: self._update_locks_status() self._update_locks_detail() - @Throttle(MIN_TIME_BETWEEN_UPDATES) + @Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES) def _update_locks_status(self): status_by_id = {} state_by_id = {} + last_status_update_by_id = {} _LOGGER.debug("Start retrieving lock and door status") for lock in self._locks: + update_start_time_utc = dt.utcnow() _LOGGER.debug("Updating lock and door status for %s", lock.device_name) try: ( @@ -315,6 +343,12 @@ class AugustData: ) = self._api.get_lock_status( self._access_token, lock.device_id, door_status=True ) + # Since there is a a race condition between calling the + # lock and activity apis, we set the last update time + # BEFORE making the api call since we will compare this + # to activity later we want activity to win over stale lock + # state. + last_status_update_by_id[lock.device_id] = update_start_time_utc except RequestException as ex: _LOGGER.error( "Request error trying to retrieve lock and door status for %s. %s", @@ -331,6 +365,17 @@ class AugustData: _LOGGER.debug("Completed retrieving lock and door status") self._lock_status_by_id = status_by_id self._door_state_by_id = state_by_id + self._lock_last_status_update_time_utc_by_id = last_status_update_by_id + + def get_last_lock_status_update_time_utc(self, lock_id): + """Return the last time that a lock status update was seen from the august API.""" + # Since the activity api is called more frequently than + # the lock api it is possible that the lock has not + # been updated yet + if lock_id not in self._lock_last_status_update_time_utc_by_id: + return dt.utc_from_timestamp(0) + + return self._lock_last_status_update_time_utc_by_id[lock_id] @Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES) def _update_locks_detail(self): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index d336e21653b..908e20e68ca 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -7,6 +7,7 @@ from august.lock import LockStatus from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.util import dt from . import DATA_AUGUST @@ -41,11 +42,23 @@ class AugustLock(LockDevice): def lock(self, **kwargs): """Lock the device.""" - self._data.lock(self._lock.device_id) + update_start_time_utc = dt.utcnow() + lock_status = self._data.lock(self._lock.device_id) + self._update_lock_status(lock_status, update_start_time_utc) def unlock(self, **kwargs): """Unlock the device.""" - self._data.unlock(self._lock.device_id) + update_start_time_utc = dt.utcnow() + lock_status = self._data.unlock(self._lock.device_id) + self._update_lock_status(lock_status, update_start_time_utc) + + def _update_lock_status(self, lock_status, update_start_time_utc): + if self._lock_status != lock_status: + self._lock_status = lock_status + self._data.update_lock_status( + self._lock.device_id, lock_status, update_start_time_utc + ) + self.schedule_update_ha_state() def update(self): """Get the latest state of the sensor.""" @@ -60,6 +73,38 @@ class AugustLock(LockDevice): if activity is not None: self._changed_by = activity.operated_by + self._sync_lock_activity(activity) + + def _sync_lock_activity(self, activity): + """Check the activity for the latest lock/unlock activity (events). + + We use this to determine the lock state in between calls to the lock + api as we update it more frequently + """ + last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc( + self._lock.device_id + ) + activity_end_time_utc = dt.as_utc(activity.activity_end_time) + + if activity_end_time_utc > last_lock_status_update_time_utc: + _LOGGER.debug( + "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]", + self.name, + activity.action, + activity_end_time_utc, + last_lock_status_update_time_utc, + ) + activity_start_time_utc = dt.as_utc(activity.activity_start_time) + if activity.action == "lock" or activity.action == "onetouchlock": + self._update_lock_status(LockStatus.LOCKED, activity_start_time_utc) + elif activity.action == "unlock": + self._update_lock_status(LockStatus.UNLOCKED, activity_start_time_utc) + else: + _LOGGER.info( + "Unhandled lock activity action %s for %s", + activity.action, + self.name, + ) @property def name(self): From 51c35ab9a8d89609fc5a0365ba424fbe080a6724 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Feb 2020 09:40:50 -0800 Subject: [PATCH 197/378] Entity Registry to store and restore name/icon (#31714) * Entity Registry to store and restore name/icon * Update test_entity_registry.py * Add original name/icon to JSON result --- .../components/config/entity_registry.py | 12 ++++---- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_platform.py | 2 ++ homeassistant/helpers/entity_registry.py | 28 +++++++++++++++++ .../components/config/test_entity_registry.py | 30 ++++++++++++++++++- tests/helpers/test_entity_registry.py | 17 +++++++++++ 6 files changed, 84 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 458a9dd3ecb..a7993017116 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -68,6 +68,7 @@ async def websocket_get_entity(hass, connection, msg): vol.Required("entity_id"): cv.entity_id, # If passed in, we update value. Passing None will remove old value. vol.Optional("name"): vol.Any(str, None), + vol.Optional("icon"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. vol.Optional("disabled_by"): vol.Any("user", None), @@ -88,11 +89,9 @@ async def websocket_update_entity(hass, connection, msg): changes = {} - if "name" in msg: - changes["name"] = msg["name"] - - if "disabled_by" in msg: - changes["disabled_by"] = msg["disabled_by"] + for key in ("name", "icon", "disabled_by"): + if key in msg: + changes[key] = msg[key] if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]: changes["new_entity_id"] = msg["new_entity_id"] @@ -151,5 +150,8 @@ def _entry_dict(entry): "disabled_by": entry.disabled_by, "entity_id": entry.entity_id, "name": entry.name, + "icon": entry.icon, "platform": entry.platform, + "original_name": entry.original_name, + "original_icon": entry.original_icon, } diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 92072b22df2..4c3b9448f5a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -337,7 +337,7 @@ class Entity(ABC): if name is not None: attr[ATTR_FRIENDLY_NAME] = name - icon = self.icon + icon = (entry and entry.icon) or self.icon if icon is not None: attr[ATTR_ICON] = icon diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e71b28f1713..e1e046eaa6d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -369,6 +369,8 @@ class EntityPlatform: supported_features=entity.supported_features, device_class=entity.device_class, unit_of_measurement=entity.unit_of_measurement, + original_name=entity.name, + original_icon=entity.icon, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 635f7feba13..05b687a8454 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -17,6 +17,8 @@ import attr from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, @@ -60,6 +62,7 @@ class RegistryEntry: unique_id = attr.ib(type=str) platform = attr.ib(type=str) name = attr.ib(type=str, default=None) + icon = attr.ib(type=str, default=None) device_id: Optional[str] = attr.ib(default=None) config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( @@ -79,6 +82,9 @@ class RegistryEntry: supported_features: int = attr.ib(default=0) device_class: Optional[str] = attr.ib(default=None) unit_of_measurement: Optional[str] = attr.ib(default=None) + # As set by integration + original_name: Optional[str] = attr.ib(default=None) + original_icon: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -167,6 +173,8 @@ class EntityRegistry: supported_features: Optional[int] = None, device_class: Optional[str] = None, unit_of_measurement: Optional[str] = None, + original_name: Optional[str] = None, + original_icon: Optional[str] = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -184,6 +192,8 @@ class EntityRegistry: supported_features=supported_features or _UNDEF, device_class=device_class or _UNDEF, unit_of_measurement=unit_of_measurement or _UNDEF, + original_name=original_name or _UNDEF, + original_icon=original_icon or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -215,6 +225,8 @@ class EntityRegistry: supported_features=supported_features or 0, device_class=device_class, unit_of_measurement=unit_of_measurement, + original_name=original_name, + original_icon=original_icon, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -254,6 +266,7 @@ class EntityRegistry: entity_id, *, name=_UNDEF, + icon=_UNDEF, new_entity_id=_UNDEF, new_unique_id=_UNDEF, disabled_by=_UNDEF, @@ -264,6 +277,7 @@ class EntityRegistry: self._async_update_entity( entity_id, name=name, + icon=icon, new_entity_id=new_entity_id, new_unique_id=new_unique_id, disabled_by=disabled_by, @@ -276,6 +290,7 @@ class EntityRegistry: entity_id, *, name=_UNDEF, + icon=_UNDEF, config_entry_id=_UNDEF, new_entity_id=_UNDEF, device_id=_UNDEF, @@ -285,6 +300,8 @@ class EntityRegistry: supported_features=_UNDEF, device_class=_UNDEF, unit_of_measurement=_UNDEF, + original_name=_UNDEF, + original_icon=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -293,6 +310,7 @@ class EntityRegistry: for attr_name, value in ( ("name", name), + ("icon", icon), ("config_entry_id", config_entry_id), ("device_id", device_id), ("disabled_by", disabled_by), @@ -300,6 +318,8 @@ class EntityRegistry: ("supported_features", supported_features), ("device_class", device_class), ("unit_of_measurement", unit_of_measurement), + ("original_name", original_name), + ("original_icon", original_icon), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -523,6 +543,14 @@ def async_setup_entity_restore( if entry.unit_of_measurement is not None: attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement + name = entry.name or entry.original_name + if name is not None: + attrs[ATTR_FRIENDLY_NAME] = name + + icon = entry.icon or entry.original_icon + if icon is not None: + attrs[ATTR_ICON] = icon + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 133c88d9ceb..8fe7e8fdbe4 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -41,6 +41,9 @@ async def test_list_entities(hass, client): "disabled_by": None, "entity_id": "test_domain.name", "name": "Hello World", + "icon": None, + "original_name": None, + "original_icon": None, "platform": "test_platform", }, { @@ -49,6 +52,9 @@ async def test_list_entities(hass, client): "disabled_by": None, "entity_id": "test_domain.no_name", "name": None, + "icon": None, + "original_name": None, + "original_icon": None, "platform": "test_platform", }, ] @@ -85,6 +91,9 @@ async def test_get_entity(hass, client): "platform": "test_platform", "entity_id": "test_domain.name", "name": "Hello World", + "icon": None, + "original_name": None, + "original_icon": None, } await client.send_json( @@ -103,6 +112,9 @@ async def test_get_entity(hass, client): "platform": "test_platform", "entity_id": "test_domain.no_name", "name": None, + "icon": None, + "original_name": None, + "original_icon": None, } @@ -117,6 +129,7 @@ async def test_update_entity(hass, client): # Using component.async_add_entities is equal to platform "domain" platform="test_platform", name="before update", + icon="icon:before update", ) }, ) @@ -127,14 +140,16 @@ async def test_update_entity(hass, client): state = hass.states.get("test_domain.world") assert state is not None assert state.name == "before update" + assert state.attributes["icon"] == "icon:before update" - # UPDATE NAME + # UPDATE NAME & ICON await client.send_json( { "id": 6, "type": "config/entity_registry/update", "entity_id": "test_domain.world", "name": "after update", + "icon": "icon:after update", } ) @@ -147,10 +162,14 @@ async def test_update_entity(hass, client): "platform": "test_platform", "entity_id": "test_domain.world", "name": "after update", + "icon": "icon:after update", + "original_name": None, + "original_icon": None, } state = hass.states.get("test_domain.world") assert state.name == "after update" + assert state.attributes["icon"] == "icon:after update" # UPDATE DISABLED_BY TO USER await client.send_json( @@ -186,6 +205,9 @@ async def test_update_entity(hass, client): "platform": "test_platform", "entity_id": "test_domain.world", "name": "after update", + "icon": "icon:after update", + "original_name": None, + "original_icon": None, } @@ -229,6 +251,9 @@ async def test_update_entity_no_changes(hass, client): "platform": "test_platform", "entity_id": "test_domain.world", "name": "name of entity", + "icon": None, + "original_name": None, + "original_icon": None, } state = hass.states.get("test_domain.world") @@ -301,6 +326,9 @@ async def test_update_entity_id(hass, client): "platform": "test_platform", "entity_id": "test_domain.planet", "name": None, + "icon": None, + "original_name": None, + "original_icon": None, } assert hass.states.get("test_domain.world") is None diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index e532d99f333..6782007ebe7 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -72,6 +72,9 @@ def test_get_or_create_updates_data(registry): supported_features=5, device_class="mock-device-class", disabled_by=entity_registry.DISABLED_HASS, + unit_of_measurement="initial-unit_of_measurement", + original_name="initial-original_name", + original_icon="initial-original_icon", ) assert orig_entry.config_entry_id == orig_config_entry.entry_id @@ -80,6 +83,9 @@ def test_get_or_create_updates_data(registry): assert orig_entry.supported_features == 5 assert orig_entry.device_class == "mock-device-class" assert orig_entry.disabled_by == entity_registry.DISABLED_HASS + assert orig_entry.unit_of_measurement == "initial-unit_of_measurement" + assert orig_entry.original_name == "initial-original_name" + assert orig_entry.original_icon == "initial-original_icon" new_config_entry = MockConfigEntry(domain="light") @@ -93,6 +99,9 @@ def test_get_or_create_updates_data(registry): supported_features=10, device_class="new-mock-device-class", disabled_by=entity_registry.DISABLED_USER, + unit_of_measurement="updated-unit_of_measurement", + original_name="updated-original_name", + original_icon="updated-original_icon", ) assert new_entry.config_entry_id == new_config_entry.entry_id @@ -100,6 +109,9 @@ def test_get_or_create_updates_data(registry): assert new_entry.capabilities == {"new-max": 100} assert new_entry.supported_features == 10 assert new_entry.device_class == "new-mock-device-class" + assert new_entry.unit_of_measurement == "updated-unit_of_measurement" + assert new_entry.original_name == "updated-original_name" + assert new_entry.original_icon == "updated-original_icon" # Should not be updated assert new_entry.disabled_by == entity_registry.DISABLED_HASS @@ -434,6 +446,7 @@ async def test_update_entity(registry): for attr_name, new_value in ( ("name", "new name"), + ("icon", "new icon"), ("disabled_by", entity_registry.DISABLED_USER), ): changes = {attr_name: new_value} @@ -503,6 +516,8 @@ async def test_restore_states(hass): capabilities={"max": 100}, supported_features=5, device_class="mock-device-class", + original_name="Mock Original Name", + original_icon="hass:original-icon", ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) @@ -524,6 +539,8 @@ async def test_restore_states(hass): "supported_features": 5, "device_class": "mock-device-class", "restored": True, + "friendly_name": "Mock Original Name", + "icon": "hass:original-icon", } registry.async_remove("light.disabled") From 3435281bd1c1962773470e231799c6a16fd73104 Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Tue, 11 Feb 2020 16:04:42 -0500 Subject: [PATCH 198/378] Support Konnected Pro alarm panel, embrace async, leverage latest HA features/architecture (#30894) * fix unique_id computation for switches * update konnected component to use async, config entries, registries. Pro board support and tests * clean up formatting comments from PR * use standard interfaces in tests * migrate config flow to use options * address latest pr feedback * format for import as part of config schema validation * address pr feedback * lint fix * simplify check based on pr feedback * clarify default schema validation * fix other schema checks * fix translations Co-authored-by: Nate Clark --- CODEOWNERS | 2 +- .../konnected/.translations/en.json | 101 ++ .../components/konnected/__init__.py | 630 ++++------ .../components/konnected/binary_sensor.py | 24 +- .../components/konnected/config_flow.py | 739 ++++++++++++ homeassistant/components/konnected/const.py | 24 +- homeassistant/components/konnected/errors.py | 10 + .../components/konnected/manifest.json | 19 +- homeassistant/components/konnected/panel.py | 359 ++++++ homeassistant/components/konnected/sensor.py | 26 +- .../components/konnected/strings.json | 101 ++ homeassistant/components/konnected/switch.py | 53 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/konnected/__init__.py | 1 + .../components/konnected/test_config_flow.py | 1052 +++++++++++++++++ tests/components/konnected/test_init.py | 601 ++++++++++ tests/components/konnected/test_panel.py | 375 ++++++ 20 files changed, 3692 insertions(+), 436 deletions(-) create mode 100644 homeassistant/components/konnected/.translations/en.json create mode 100644 homeassistant/components/konnected/config_flow.py create mode 100644 homeassistant/components/konnected/errors.py create mode 100644 homeassistant/components/konnected/panel.py create mode 100644 homeassistant/components/konnected/strings.json create mode 100644 tests/components/konnected/__init__.py create mode 100644 tests/components/konnected/test_config_flow.py create mode 100644 tests/components/konnected/test_init.py create mode 100644 tests/components/konnected/test_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index 8f44c3caebc..658fca1e8fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -186,7 +186,7 @@ homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills -homeassistant/components/konnected/* @heythisisnate +homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json new file mode 100644 index 00000000000..6459fcebc53 --- /dev/null +++ b/homeassistant/components/konnected/.translations/en.json @@ -0,0 +1,101 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "not_konn_panel": "Not a recognized Konnected.io device", + "unknown": "Unknown error occurred" + }, + "error": { + "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}" + }, + "step": { + "confirm": { + "description": "Model: {model}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings.", + "title": "Konnected Device Ready" + }, + "user": { + "data": { + "host": "Konnected device IP address", + "port": "Konnected device port" + }, + "description": "Please enter the host information for your Konnected Panel.", + "title": "Discover Konnected Device" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Not a recognized Konnected.io device" + }, + "error": {}, + "step": { + "options_binary": { + "data": { + "inverse": "Invert the open/close state", + "name": "Name (optional)", + "type": "Binary Sensor Type" + }, + "description": "Please select the options for the binary sensor attached to {zone}", + "title": "Configure Binary Sensor" + }, + "options_digital": { + "data": { + "name": "Name (optional)", + "poll_interval": "Poll Interval (minutes) (optional)", + "type": "Sensor Type" + }, + "description": "Please select the options for the digital sensor attached to {zone}", + "title": "Configure Digital Sensor" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + }, + "description": "Discovered a {model} at {host}. Select the base configuration of each I/O below - depending on the I/O it may allow for binary sensors (open/close contacts), digital sensors (dht and ds18b20), or switchable outputs. You'll be able to configure detailed options in the next steps.", + "title": "Configure I/O" + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.", + "title": "Configure Extended I/O" + }, + "options_misc": { + "data": { + "blink": "Blink panel LED on when sending state change" + }, + "description": "Please select the desired behavior for your panel", + "title": "Configure Misc" + }, + "options_switch": { + "data": { + "activation": "Output when on", + "momentary": "Pulse duration (ms) (optional)", + "name": "Name (optional)", + "pause": "Pause between pulses (ms) (optional)", + "repeat": "Times to repeat (-1=infinite) (optional)" + }, + "description": "Please select the output options for {zone}", + "title": "Configure Switchable Output" + } + }, + "title": "Konnected Alarm Panel Options" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 28e62c322ad..94508b01483 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -1,20 +1,20 @@ """Support for Konnected devices.""" import asyncio +import copy import hmac import json import logging from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response -import konnected import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA -from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_STATE, CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, CONF_DEVICES, @@ -27,45 +27,106 @@ from homeassistant.const import ( CONF_SWITCHES, CONF_TYPE, CONF_ZONE, - EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, + STATE_OFF, STATE_ON, ) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from .config_flow import ( # Loading the config flow file will register the flow + CONF_DEFAULT_OPTIONS, + CONF_IO, + CONF_IO_BIN, + CONF_IO_DIG, + CONF_IO_SWI, + OPTIONS_SCHEMA, +) from .const import ( CONF_ACTIVATION, CONF_API_HOST, CONF_BLINK, - CONF_DHT_SENSORS, CONF_DISCOVERY, - CONF_DS18B20_SENSORS, CONF_INVERSE, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, CONF_REPEAT, DOMAIN, - ENDPOINT_ROOT, PIN_TO_ZONE, - SIGNAL_SENSOR_UPDATE, STATE_HIGH, STATE_LOW, UPDATE_ENDPOINT, ZONE_TO_PIN, + ZONES, ) +from .errors import CannotConnect from .handlers import HANDLERS +from .panel import AlarmPanel _LOGGER = logging.getLogger(__name__) -_BINARY_SENSOR_SCHEMA = vol.All( + +def ensure_pin(value): + """Check if valid pin and coerce to string.""" + if value is None: + raise vol.Invalid("pin value is None") + + if PIN_TO_ZONE.get(str(value)) is None: + raise vol.Invalid("pin not valid") + + return str(value) + + +def ensure_zone(value): + """Check if valid zone and coerce to string.""" + if value is None: + raise vol.Invalid("zone value is None") + + if str(value) not in ZONES is None: + raise vol.Invalid("zone not valid") + + return str(value) + + +def import_validator(config): + """Validate zones and reformat for import.""" + config = copy.deepcopy(config) + io_cfgs = {} + # Replace pins with zones + for conf_platform, conf_io in ( + (CONF_BINARY_SENSORS, CONF_IO_BIN), + (CONF_SENSORS, CONF_IO_DIG), + (CONF_SWITCHES, CONF_IO_SWI), + ): + for zone in config.get(conf_platform, []): + if zone.get(CONF_PIN): + zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]] + del zone[CONF_PIN] + io_cfgs[zone[CONF_ZONE]] = conf_io + + # Migrate config_entry data into default_options structure + config[CONF_IO] = io_cfgs + config[CONF_DEFAULT_OPTIONS] = OPTIONS_SCHEMA(config) + + # clean up fields migrated to options + config.pop(CONF_BINARY_SENSORS, None) + config.pop(CONF_SENSORS, None) + config.pop(CONF_SWITCHES, None) + config.pop(CONF_BLINK, None) + config.pop(CONF_DISCOVERY, None) + config.pop(CONF_IO, None) + return config + + +# configuration.yaml schemas (legacy) +BINARY_SENSOR_SCHEMA_YAML = vol.All( vol.Schema( { - vol.Exclusive(CONF_PIN, "s_pin"): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, "s_pin"): vol.Any(*ZONE_TO_PIN), + vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone, + vol.Exclusive(CONF_PIN, "s_io"): ensure_pin, vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_INVERSE, default=False): cv.boolean, @@ -74,14 +135,14 @@ _BINARY_SENSOR_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PIN, CONF_ZONE), ) -_SENSOR_SCHEMA = vol.All( +SENSOR_SCHEMA_YAML = vol.All( vol.Schema( { - vol.Exclusive(CONF_PIN, "s_pin"): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, "s_pin"): vol.Any(*ZONE_TO_PIN), + vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone, + vol.Exclusive(CONF_PIN, "s_io"): ensure_pin, vol.Required(CONF_TYPE): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])), vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_POLL_INTERVAL): vol.All( + vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All( vol.Coerce(int), vol.Range(min=1) ), } @@ -89,11 +150,11 @@ _SENSOR_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PIN, CONF_ZONE), ) -_SWITCH_SCHEMA = vol.All( +SWITCH_SCHEMA_YAML = vol.All( vol.Schema( { - vol.Exclusive(CONF_PIN, "a_pin"): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, "a_pin"): vol.Any(*ZONE_TO_PIN), + vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone, + vol.Exclusive(CONF_PIN, "s_io"): ensure_pin, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All( vol.Lower, vol.Any(STATE_HIGH, STATE_LOW) @@ -106,6 +167,24 @@ _SWITCH_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PIN, CONF_ZONE), ) +DEVICE_SCHEMA_YAML = vol.All( + vol.Schema( + { + vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSOR_SCHEMA_YAML] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA_YAML]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA_YAML]), + vol.Inclusive(CONF_HOST, "host_info"): cv.string, + vol.Inclusive(CONF_PORT, "host_info"): cv.port, + vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + } + ), + import_validator, +) + # pylint: disable=no-value-for-parameter CONFIG_SCHEMA = vol.Schema( { @@ -113,352 +192,88 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_API_HOST): vol.Url(), - vol.Required(CONF_DEVICES): [ - { - vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [_BINARY_SENSOR_SCHEMA] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [_SENSOR_SCHEMA] - ), - vol.Optional(CONF_SWITCHES): vol.All( - cv.ensure_list, [_SWITCH_SCHEMA] - ), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_BLINK, default=True): cv.boolean, - vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, - } - ], + vol.Optional(CONF_DEVICES): vol.All( + cv.ensure_list, [DEVICE_SCHEMA_YAML] + ), } ) }, extra=vol.ALLOW_EXTRA, ) +YAML_CONFIGS = "yaml_configs" +PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistant, config: dict): """Set up the Konnected platform.""" cfg = config.get(DOMAIN) if cfg is None: cfg = {} - access_token = cfg.get(CONF_ACCESS_TOKEN) if DOMAIN not in hass.data: hass.data[DOMAIN] = { - CONF_ACCESS_TOKEN: access_token, + CONF_ACCESS_TOKEN: cfg.get(CONF_ACCESS_TOKEN), CONF_API_HOST: cfg.get(CONF_API_HOST), + CONF_DEVICES: {}, } - def setup_device(host, port): - """Set up a Konnected device at `host` listening on `port`.""" - discovered = DiscoveredDevice(hass, host, port) - if discovered.is_configured: - discovered.setup() - else: - _LOGGER.warning( - "Konnected device %s was discovered on the network" - " but not specified in configuration.yaml", - discovered.device_id, + hass.http.register_view(KonnectedView) + + # Check if they have yaml configured devices + if CONF_DEVICES not in cfg: + return True + + for device in cfg.get(CONF_DEVICES, []): + # Attempt to importing the cfg. Use + # hass.async_add_job to avoid a deadlock. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=device, ) - - def device_discovered(service, info): - """Call when a Konnected device has been discovered.""" - host = info.get(CONF_HOST) - port = info.get(CONF_PORT) - setup_device(host, port) - - async def manual_discovery(event): - """Init devices on the network with manually assigned addresses.""" - specified = [ - dev - for dev in cfg.get(CONF_DEVICES) - if dev.get(CONF_HOST) and dev.get(CONF_PORT) - ] - - while specified: - for dev in specified: - _LOGGER.debug( - "Discovering Konnected device %s at %s:%s", - dev.get(CONF_ID), - dev.get(CONF_HOST), - dev.get(CONF_PORT), - ) - try: - await hass.async_add_executor_job( - setup_device, dev.get(CONF_HOST), dev.get(CONF_PORT) - ) - specified.remove(dev) - except konnected.Client.ClientError as err: - _LOGGER.error(err) - await asyncio.sleep(10) # try again in 10 seconds - - # Initialize devices specified in the configuration on boot - for device in cfg.get(CONF_DEVICES): - ConfiguredDevice(hass, device, config).save_data() - - discovery.async_listen(hass, SERVICE_KONNECTED, device_discovered) - - hass.http.register_view(KonnectedView(access_token)) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery) - + ) return True -class ConfiguredDevice: - """A representation of a configured Konnected device.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up panel from a config entry.""" + client = AlarmPanel(hass, entry) + # create a data store in hass.data[DOMAIN][CONF_DEVICES] + await client.async_save_data() - def __init__(self, hass, config, hass_config): - """Initialize the Konnected device.""" - self.hass = hass - self.config = config - self.hass_config = hass_config + try: + await client.async_connect() + except CannotConnect: + # this will trigger a retry in the future + raise config_entries.ConfigEntryNotReady - @property - def device_id(self): - """Device id is the MAC address as string with punctuation removed.""" - return self.config.get(CONF_ID) - - def save_data(self): - """Save the device configuration to `hass.data`.""" - binary_sensors = {} - for entity in self.config.get(CONF_BINARY_SENSORS) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - binary_sensors[pin] = { - CONF_TYPE: entity[CONF_TYPE], - CONF_NAME: entity.get( - CONF_NAME, - "Konnected {} Zone {}".format(self.device_id[6:], PIN_TO_ZONE[pin]), - ), - CONF_INVERSE: entity.get(CONF_INVERSE), - ATTR_STATE: None, - } - _LOGGER.debug( - "Set up binary_sensor %s (initial state: %s)", - binary_sensors[pin].get("name"), - binary_sensors[pin].get(ATTR_STATE), - ) - - actuators = [] - for entity in self.config.get(CONF_SWITCHES) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - act = { - CONF_PIN: pin, - CONF_NAME: entity.get( - CONF_NAME, - "Konnected {} Actuator {}".format( - self.device_id[6:], PIN_TO_ZONE[pin] - ), - ), - ATTR_STATE: None, - CONF_ACTIVATION: entity[CONF_ACTIVATION], - CONF_MOMENTARY: entity.get(CONF_MOMENTARY), - CONF_PAUSE: entity.get(CONF_PAUSE), - CONF_REPEAT: entity.get(CONF_REPEAT), - } - actuators.append(act) - _LOGGER.debug("Set up switch %s", act) - - sensors = [] - for entity in self.config.get(CONF_SENSORS) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - sensor = { - CONF_PIN: pin, - CONF_NAME: entity.get( - CONF_NAME, - "Konnected {} Sensor {}".format( - self.device_id[6:], PIN_TO_ZONE[pin] - ), - ), - CONF_TYPE: entity[CONF_TYPE], - CONF_POLL_INTERVAL: entity.get(CONF_POLL_INTERVAL), - } - sensors.append(sensor) - _LOGGER.debug( - "Set up %s sensor %s (initial state: %s)", - sensor.get(CONF_TYPE), - sensor.get(CONF_NAME), - sensor.get(ATTR_STATE), - ) - - device_data = { - CONF_BINARY_SENSORS: binary_sensors, - CONF_SENSORS: sensors, - CONF_SWITCHES: actuators, - CONF_BLINK: self.config.get(CONF_BLINK), - CONF_DISCOVERY: self.config.get(CONF_DISCOVERY), - } - - if CONF_DEVICES not in self.hass.data[DOMAIN]: - self.hass.data[DOMAIN][CONF_DEVICES] = {} - - _LOGGER.debug( - "Storing data in hass.data[%s][%s][%s]: %s", - DOMAIN, - CONF_DEVICES, - self.device_id, - device_data, + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) ) - self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data - - for platform in ["binary_sensor", "sensor", "switch"]: - discovery.load_platform( - self.hass, - platform, - DOMAIN, - {"device_id": self.device_id}, - self.hass_config, - ) + entry.add_update_listener(async_entry_updated) + return True -class DiscoveredDevice: - """A representation of a discovered Konnected device.""" - - def __init__(self, hass, host, port): - """Initialize the Konnected device.""" - self.hass = hass - self.host = host - self.port = port - - self.client = konnected.Client(host, str(port)) - self.status = self.client.get_status() - - def setup(self): - """Set up a newly discovered Konnected device.""" - _LOGGER.info( - "Discovered Konnected device %s. Open http://%s:%s in a " - "web browser to view device status.", - self.device_id, - self.host, - self.port, +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 + ] ) - self.save_data() - self.update_initial_states() - self.sync_device_config() + ) + if unload_ok: + hass.data[DOMAIN][CONF_DEVICES].pop(entry.data[CONF_ID]) - def save_data(self): - """Save the discovery information to `hass.data`.""" - self.stored_configuration["client"] = self.client - self.stored_configuration["host"] = self.host - self.stored_configuration["port"] = self.port + return unload_ok - @property - def device_id(self): - """Device id is the MAC address as string with punctuation removed.""" - return self.status["mac"].replace(":", "") - @property - def is_configured(self): - """Return true if device_id is specified in the configuration.""" - return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)) - - @property - def stored_configuration(self): - """Return the configuration stored in `hass.data` for this device.""" - return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) - - def binary_sensor_configuration(self): - """Return the configuration map for syncing binary sensors.""" - return [{"pin": p} for p in self.stored_configuration[CONF_BINARY_SENSORS]] - - def actuator_configuration(self): - """Return the configuration map for syncing actuators.""" - return [ - { - "pin": data.get(CONF_PIN), - "trigger": (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] else 1), - } - for data in self.stored_configuration[CONF_SWITCHES] - ] - - def dht_sensor_configuration(self): - """Return the configuration map for syncing DHT sensors.""" - return [ - {CONF_PIN: sensor[CONF_PIN], CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]} - for sensor in self.stored_configuration[CONF_SENSORS] - if sensor[CONF_TYPE] == "dht" - ] - - def ds18b20_sensor_configuration(self): - """Return the configuration map for syncing DS18B20 sensors.""" - return [ - {"pin": sensor[CONF_PIN]} - for sensor in self.stored_configuration[CONF_SENSORS] - if sensor[CONF_TYPE] == "ds18b20" - ] - - def update_initial_states(self): - """Update the initial state of each sensor from status poll.""" - for sensor_data in self.status.get("sensors"): - sensor_config = self.stored_configuration[CONF_BINARY_SENSORS].get( - sensor_data.get(CONF_PIN), {} - ) - entity_id = sensor_config.get(ATTR_ENTITY_ID) - - state = bool(sensor_data.get(ATTR_STATE)) - if sensor_config.get(CONF_INVERSE): - state = not state - - dispatcher_send(self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) - - def desired_settings_payload(self): - """Return a dict representing the desired device configuration.""" - desired_api_host = ( - self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url - ) - desired_api_endpoint = desired_api_host + ENDPOINT_ROOT - - return { - "sensors": self.binary_sensor_configuration(), - "actuators": self.actuator_configuration(), - "dht_sensors": self.dht_sensor_configuration(), - "ds18b20_sensors": self.ds18b20_sensor_configuration(), - "auth_token": self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), - "endpoint": desired_api_endpoint, - "blink": self.stored_configuration.get(CONF_BLINK), - "discovery": self.stored_configuration.get(CONF_DISCOVERY), - } - - def current_settings_payload(self): - """Return a dict of configuration currently stored on the device.""" - settings = self.status["settings"] - if not settings: - settings = {} - - return { - "sensors": [{"pin": s[CONF_PIN]} for s in self.status.get("sensors")], - "actuators": self.status.get("actuators"), - "dht_sensors": self.status.get(CONF_DHT_SENSORS), - "ds18b20_sensors": self.status.get(CONF_DS18B20_SENSORS), - "auth_token": settings.get("token"), - "endpoint": settings.get("apiUrl"), - "blink": settings.get(CONF_BLINK), - "discovery": settings.get(CONF_DISCOVERY), - } - - def sync_device_config(self): - """Sync the new pin configuration to the Konnected device if needed.""" - _LOGGER.debug( - "Device %s settings payload: %s", - self.device_id, - self.desired_settings_payload(), - ) - if self.desired_settings_payload() != self.current_settings_payload(): - _LOGGER.info("pushing settings to device %s", self.device_id) - self.client.put_settings(**self.desired_settings_payload()) +async def async_entry_updated(hass: HomeAssistant, entry: ConfigEntry): + """Reload the config entry when options change.""" + await hass.config_entries.async_reload(entry.entry_id) class KonnectedView(HomeAssistantView): @@ -468,9 +283,8 @@ class KonnectedView(HomeAssistantView): name = "api:konnected" requires_auth = False # Uses access token from configuration - def __init__(self, auth_token): + def __init__(self): """Initialize the view.""" - self.auth_token = auth_token @staticmethod def binary_value(state, activation): @@ -479,50 +293,29 @@ class KonnectedView(HomeAssistantView): return 1 if state == STATE_ON else 0 return 0 if state == STATE_ON else 1 - async def get(self, request: Request, device_id) -> Response: - """Return the current binary state of a switch.""" + async def update_sensor(self, request: Request, device_id) -> Response: + """Process a put or post.""" hass = request.app["hass"] - pin_num = int(request.query.get("pin")) data = hass.data[DOMAIN] - device = data[CONF_DEVICES][device_id] - if not device: - return self.json_message( - f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND - ) - - try: - pin = next( - filter( - lambda switch: switch[CONF_PIN] == pin_num, device[CONF_SWITCHES] - ) - ) - except StopIteration: - pin = None - - if not pin: - return self.json_message( - format("Switch on pin {} not configured", pin_num), - status_code=HTTP_NOT_FOUND, - ) - - return self.json( - { - "pin": pin_num, - "state": self.binary_value( - hass.states.get(pin[ATTR_ENTITY_ID]).state, pin[CONF_ACTIVATION] - ), - } + auth = request.headers.get(AUTHORIZATION, None) + tokens = [] + if hass.data[DOMAIN].get(CONF_ACCESS_TOKEN): + tokens.extend([hass.data[DOMAIN][CONF_ACCESS_TOKEN]]) + tokens.extend( + [ + entry.data[CONF_ACCESS_TOKEN] + for entry in hass.config_entries.async_entries(DOMAIN) + ] ) - - async def put(self, request: Request, device_id) -> Response: - """Receive a sensor update via PUT request and async set state.""" - hass = request.app["hass"] - data = hass.data[DOMAIN] + if auth is None or not next( + (True for token in tokens if hmac.compare_digest(f"Bearer {token}", auth)), + False, + ): + return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED) try: # Konnected 2.2.0 and above supports JSON payloads payload = await request.json() - pin_num = payload["pin"] except json.decoder.JSONDecodeError: _LOGGER.error( ( @@ -532,30 +325,97 @@ class KonnectedView(HomeAssistantView): ) ) - auth = request.headers.get(AUTHORIZATION, None) - if not hmac.compare_digest(f"Bearer {self.auth_token}", auth): - return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED) - pin_num = int(pin_num) device = data[CONF_DEVICES].get(device_id) if device is None: return self.json_message( "unregistered device", status_code=HTTP_BAD_REQUEST ) - pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or next( - (s for s in device[CONF_SENSORS] if s[CONF_PIN] == pin_num), None - ) - if pin_data is None: + try: + zone_num = str(payload.get(CONF_ZONE) or PIN_TO_ZONE[payload[CONF_PIN]]) + zone_data = device[CONF_BINARY_SENSORS].get(zone_num) or next( + (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None + ) + except KeyError: + zone_data = None + + if zone_data is None: return self.json_message( "unregistered sensor/actuator", status_code=HTTP_BAD_REQUEST ) - pin_data["device_id"] = device_id + zone_data["device_id"] = device_id for attr in ["state", "temp", "humi", "addr"]: value = payload.get(attr) handler = HANDLERS.get(attr) if value is not None and handler: - hass.async_create_task(handler(hass, pin_data, payload)) + hass.async_create_task(handler(hass, zone_data, payload)) return self.json_message("ok") + + async def get(self, request: Request, device_id) -> Response: + """Return the current binary state of a switch.""" + hass = request.app["hass"] + data = hass.data[DOMAIN] + + device = data[CONF_DEVICES].get(device_id) + if not device: + return self.json_message( + f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND + ) + + # Our data model is based on zone ids but we convert from/to pin ids + # based on whether they are specified in the request + try: + zone_num = str( + request.query.get(CONF_ZONE) or PIN_TO_ZONE[request.query[CONF_PIN]] + ) + zone = next( + ( + switch + for switch in device[CONF_SWITCHES] + if switch[CONF_ZONE] == zone_num + ) + ) + + except StopIteration: + zone = None + except KeyError: + zone = None + zone_num = None + + if not zone: + target = request.query.get( + CONF_ZONE, request.query.get(CONF_PIN, "unknown") + ) + return self.json_message( + f"Switch on zone or pin {target} not configured", + status_code=HTTP_NOT_FOUND, + ) + + resp = {} + if request.query.get(CONF_ZONE): + resp[CONF_ZONE] = zone_num + else: + resp[CONF_PIN] = ZONE_TO_PIN[zone_num] + + # Make sure entity is setup + zone_entity_id = zone.get(ATTR_ENTITY_ID) + if zone_entity_id: + resp["state"] = self.binary_value( + hass.states.get(zone_entity_id).state, zone[CONF_ACTIVATION], + ) + return self.json(resp) + + _LOGGER.warning("Konnected entity not yet setup, returning default") + resp["state"] = self.binary_value(STATE_OFF, zone[CONF_ACTIVATION]) + return self.json(resp) + + async def put(self, request: Request, device_id) -> Response: + """Receive a sensor update via PUT request and async set state.""" + return await self.update_sensor(request, device_id) + + async def post(self, request: Request, device_id) -> Response: + """Receive a sensor update via POST request and async set state.""" + return await self.update_sensor(request, device_id) diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index 486c228d6fb..dc4dae7787f 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -13,18 +13,15 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE +from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_SENSOR_UPDATE _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up binary sensors attached to a Konnected device.""" - if discovery_info is None: - return - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up binary sensors attached to a Konnected device from a config entry.""" data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info["device_id"] + device_id = config_entry.data["id"] sensors = [ KonnectedBinarySensor(device_id, pin_num, pin_data) for pin_num, pin_data in data[CONF_DEVICES][device_id][ @@ -37,14 +34,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KonnectedBinarySensor(BinarySensorDevice): """Representation of a Konnected binary sensor.""" - def __init__(self, device_id, pin_num, data): + def __init__(self, device_id, zone_num, data): """Initialize the Konnected binary sensor.""" self._data = data self._device_id = device_id - self._pin_num = pin_num + self._zone_num = zone_num self._state = self._data.get(ATTR_STATE) self._device_class = self._data.get(CONF_TYPE) - self._unique_id = "{}-{}".format(device_id, PIN_TO_ZONE[pin_num]) + self._unique_id = f"{device_id}-{zone_num}" self._name = self._data.get(CONF_NAME) @property @@ -72,6 +69,13 @@ class KonnectedBinarySensor(BinarySensorDevice): """Return the device class.""" return self._device_class + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, + } + async def async_added_to_hass(self): """Store entity_id and register state change callback.""" self._data[ATTR_ENTITY_ID] = self.entity_id diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py new file mode 100644 index 00000000000..447211308ae --- /dev/null +++ b/homeassistant/components/konnected/config_flow.py @@ -0,0 +1,739 @@ +"""Config flow for konnected.io integration.""" +import asyncio +import copy +import logging +import random +import string +from urllib.parse import urlparse + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASSES_SCHEMA, +) +from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_BINARY_SENSORS, + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TYPE, + CONF_ZONE, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_ACTIVATION, + CONF_BLINK, + CONF_DISCOVERY, + CONF_INVERSE, + CONF_MODEL, + CONF_MOMENTARY, + CONF_PAUSE, + CONF_POLL_INTERVAL, + CONF_REPEAT, + DOMAIN, + STATE_HIGH, + STATE_LOW, + ZONES, +) +from .errors import CannotConnect +from .panel import KONN_MODEL, KONN_MODEL_PRO, get_status + +_LOGGER = logging.getLogger(__name__) + +ATTR_KONN_UPNP_MODEL_NAME = "model_name" # standard upnp is modelName +CONF_IO = "io" +CONF_IO_DIS = "Disabled" +CONF_IO_BIN = "Binary Sensor" +CONF_IO_DIG = "Digital Sensor" +CONF_IO_SWI = "Switchable Output" + +KONN_MANUFACTURER = "konnected.io" +KONN_PANEL_MODEL_NAMES = { + KONN_MODEL: "Konnected Alarm Panel", + KONN_MODEL_PRO: "Konnected Alarm Panel Pro", +} + +OPTIONS_IO_ANY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG, CONF_IO_SWI]) +OPTIONS_IO_INPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG]) +OPTIONS_IO_OUTPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_SWI]) + + +# Config entry schemas +IO_SCHEMA = vol.Schema( + { + vol.Optional("1", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("2", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("3", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("4", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("5", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("6", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("7", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("8", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("9", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("10", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("11", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("12", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("out", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + vol.Optional("alarm1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + vol.Optional("out1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + vol.Optional("alarm2_out2", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + } +) + +BINARY_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(ZONES), + vol.Required(CONF_TYPE, default=DEVICE_CLASS_DOOR): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERSE, default=False): cv.boolean, + } +) + +SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(ZONES), + vol.Required(CONF_TYPE, default="dht"): vol.All( + vol.Lower, vol.In(["dht", "ds18b20"]) + ), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } +) + +SWITCH_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(ZONES), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All( + vol.Lower, vol.Any(STATE_HIGH, STATE_LOW) + ), + vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional(CONF_REPEAT): vol.All(vol.Coerce(int), vol.Range(min=-1)), + } +) + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_IO): IO_SCHEMA, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSOR_SCHEMA] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), + vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + }, + extra=vol.REMOVE_EXTRA, +) + +CONF_DEFAULT_OPTIONS = "default_options" +CONFIG_ENTRY_SCHEMA = vol.Schema( + { + vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_MODEL): vol.Any(*KONN_PANEL_MODEL_NAMES), + vol.Required(CONF_ACCESS_TOKEN): cv.matches_regex("[a-zA-Z0-9]+"), + vol.Required(CONF_DEFAULT_OPTIONS): OPTIONS_SCHEMA, + }, + extra=vol.REMOVE_EXTRA, +) + + +class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NEW_NAME.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + + def __init__(self): + """Initialize the Konnected flow.""" + self.data = {} + self.options = OPTIONS_SCHEMA({CONF_IO: {}}) + + async def async_gen_config(self, host, port): + """Populate self.data based on panel status. + + This will raise CannotConnect if an error occurs + """ + self.data[CONF_HOST] = host + self.data[CONF_PORT] = port + try: + status = await get_status(self.hass, host, port) + self.data[CONF_ID] = status["mac"].replace(":", "") + except (CannotConnect, KeyError): + raise CannotConnect + else: + self.data[CONF_MODEL] = status.get("name", KONN_MODEL) + self.data[CONF_ACCESS_TOKEN] = "".join( + random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) + ) + + async def async_step_import(self, device_config): + """Import a configuration.yaml config. + + This flow is triggered by `async_setup` for configured panels. + """ + _LOGGER.debug(device_config) + + # save the data and confirm connection via user step + await self.async_set_unique_id(device_config["id"]) + self.options = device_config[CONF_DEFAULT_OPTIONS] + + # config schema ensures we have port if we have host + if device_config.get(CONF_HOST): + return await self.async_step_user( + user_input={ + CONF_HOST: device_config[CONF_HOST], + CONF_PORT: device_config[CONF_PORT], + } + ) + + # if we have no host info wait for it or abort if previously configured + self._abort_if_unique_id_configured() + return await self.async_step_user() + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered konnected panel. + + This flow is triggered by the SSDP component. It will check if the + device is already configured and attempt to finish the config if not. + """ + _LOGGER.debug(discovery_info) + + try: + if discovery_info[ATTR_UPNP_MANUFACTURER] != KONN_MANUFACTURER: + return self.async_abort(reason="not_konn_panel") + + if not any( + name in discovery_info[ATTR_UPNP_MODEL_NAME] + for name in KONN_PANEL_MODEL_NAMES + ): + _LOGGER.warning( + "Discovered unrecognized Konnected device %s", + discovery_info.get(ATTR_UPNP_MODEL_NAME, "Unknown"), + ) + return self.async_abort(reason="not_konn_panel") + + # If MAC is missing it is a bug in the device fw but we'll guard + # against it since the field is so vital + except KeyError: + _LOGGER.error("Malformed Konnected SSDP info") + else: + # extract host/port from ssdp_location + netloc = urlparse(discovery_info["ssdp_location"]).netloc.split(":") + return await self.async_step_user( + user_input={CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} + ) + + return self.async_abort(reason="unknown") + + async def async_step_user(self, user_input=None): + """Connect to panel and get config.""" + errors = {} + if user_input: + # build config info and wait for user confirmation + self.data[CONF_HOST] = user_input[CONF_HOST] + self.data[CONF_PORT] = user_input[CONF_PORT] + self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get( + CONF_ACCESS_TOKEN + ) or "".join( + random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) + ) + + # brief delay to allow processing of recent status req + await asyncio.sleep(0.1) + try: + status = await get_status( + self.hass, self.data[CONF_HOST], self.data[CONF_PORT] + ) + except CannotConnect: + errors["base"] = "cannot_connect" + else: + self.data[CONF_ID] = status["mac"].replace(":", "") + self.data[CONF_MODEL] = status.get("name", KONN_MODEL) + return await self.async_step_confirm() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self.data.get(CONF_HOST)): str, + vol.Required(CONF_PORT, default=self.data.get(CONF_PORT)): int, + } + ), + errors=errors, + ) + + async def async_step_confirm(self, user_input=None): + """Attempt to link with the Konnected panel. + + Given a configured host, will ask the user to confirm and finalize + the connection. + """ + if user_input is None: + # update an existing config entry if host info changes + entry = await self.async_set_unique_id( + self.data[CONF_ID], raise_on_progress=False + ) + if entry and ( + entry.data[CONF_HOST] != self.data[CONF_HOST] + or entry.data[CONF_PORT] != self.data[CONF_PORT] + ): + entry_data = copy.deepcopy(entry.data) + entry_data.update(self.data) + self.hass.config_entries.async_update_entry(entry, data=entry_data) + + self._abort_if_unique_id_configured() + return self.async_show_form( + step_id="confirm", + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], + "host": self.data[CONF_HOST], + "port": self.data[CONF_PORT], + }, + ) + + # Attach default options and create entry + self.data[CONF_DEFAULT_OPTIONS] = self.options + return self.async_create_entry( + title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], data=self.data, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return the Options Flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for a Konnected Panel.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.entry = config_entry + self.model = self.entry.data[CONF_MODEL] + self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS] + + # as config proceeds we'll build up new options and then replace what's in the config entry + self.new_opt = {CONF_IO: {}} + self.active_cfg = None + self.io_cfg = {} + + @callback + def get_current_cfg(self, io_type, zone): + """Get the current zone config.""" + return next( + ( + cfg + for cfg in self.current_opt.get(io_type, []) + if cfg[CONF_ZONE] == zone + ), + {}, + ) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + return await self.async_step_options_io() + + async def async_step_options_io(self, user_input=None): + """Configure legacy panel IO or first half of pro IO.""" + errors = {} + current_io = self.current_opt.get(CONF_IO, {}) + + if user_input is not None: + # strip out disabled io and save for options cfg + for key, value in user_input.items(): + if value != CONF_IO_DIS: + self.new_opt[CONF_IO][key] = value + return await self.async_step_options_io_ext() + + if self.model == KONN_MODEL: + return self.async_show_form( + step_id="options_io", + data_schema=vol.Schema( + { + vol.Required( + "1", default=current_io.get("1", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "2", default=current_io.get("2", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "3", default=current_io.get("3", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "4", default=current_io.get("4", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "5", default=current_io.get("5", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "6", default=current_io.get("6", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "out", default=current_io.get("out", CONF_IO_DIS) + ): OPTIONS_IO_OUTPUT_ONLY, + } + ), + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.model], + "host": self.entry.data[CONF_HOST], + }, + errors=errors, + ) + + # configure the first half of the pro board io + if self.model == KONN_MODEL_PRO: + return self.async_show_form( + step_id="options_io", + data_schema=vol.Schema( + { + vol.Required( + "1", default=current_io.get("1", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "2", default=current_io.get("2", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "3", default=current_io.get("3", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "4", default=current_io.get("4", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "5", default=current_io.get("5", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "6", default=current_io.get("6", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "7", default=current_io.get("7", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + } + ), + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.model], + "host": self.entry.data[CONF_HOST], + }, + errors=errors, + ) + + return self.async_abort(reason="not_konn_panel") + + async def async_step_options_io_ext(self, user_input=None): + """Allow the user to configure the extended IO for pro.""" + errors = {} + current_io = self.current_opt.get(CONF_IO, {}) + + if user_input is not None: + # strip out disabled io and save for options cfg + for key, value in user_input.items(): + if value != CONF_IO_DIS: + self.new_opt[CONF_IO].update({key: value}) + self.io_cfg = copy.deepcopy(self.new_opt[CONF_IO]) + return await self.async_step_options_binary() + + if self.model == KONN_MODEL: + self.io_cfg = copy.deepcopy(self.new_opt[CONF_IO]) + return await self.async_step_options_binary() + + if self.model == KONN_MODEL_PRO: + return self.async_show_form( + step_id="options_io_ext", + data_schema=vol.Schema( + { + vol.Required( + "8", default=current_io.get("8", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "9", default=current_io.get("9", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "10", default=current_io.get("10", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "11", default=current_io.get("11", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "12", default=current_io.get("12", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "alarm1", default=current_io.get("alarm1", CONF_IO_DIS) + ): OPTIONS_IO_OUTPUT_ONLY, + vol.Required( + "out1", default=current_io.get("out1", CONF_IO_DIS) + ): OPTIONS_IO_OUTPUT_ONLY, + vol.Required( + "alarm2_out2", + default=current_io.get("alarm2_out2", CONF_IO_DIS), + ): OPTIONS_IO_OUTPUT_ONLY, + } + ), + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.model], + "host": self.entry.data[CONF_HOST], + }, + errors=errors, + ) + + return self.async_abort(reason="not_konn_panel") + + async def async_step_options_binary(self, user_input=None): + """Allow the user to configure the IO options for binary sensors.""" + errors = {} + if user_input is not None: + zone = {"zone": self.active_cfg} + zone.update(user_input) + self.new_opt[CONF_BINARY_SENSORS] = self.new_opt.get( + CONF_BINARY_SENSORS, [] + ) + [zone] + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None + + if self.active_cfg: + current_cfg = self.get_current_cfg(CONF_BINARY_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_binary", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, + default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR), + ): DEVICE_CLASSES_SCHEMA, + vol.Optional( + CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_INVERSE, default=current_cfg.get(CONF_INVERSE, False) + ): bool, + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper + }, + errors=errors, + ) + + # find the next unconfigured binary sensor + for key, value in self.io_cfg.items(): + if value == CONF_IO_BIN: + self.active_cfg = key + current_cfg = self.get_current_cfg(CONF_BINARY_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_binary", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, + default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR), + ): DEVICE_CLASSES_SCHEMA, + vol.Optional( + CONF_NAME, + default=current_cfg.get(CONF_NAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_INVERSE, + default=current_cfg.get(CONF_INVERSE, False), + ): bool, + } + ), + description_placeholders={ + "zone": "Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper + }, + errors=errors, + ) + + return await self.async_step_options_digital() + + async def async_step_options_digital(self, user_input=None): + """Allow the user to configure the IO options for digital sensors.""" + errors = {} + if user_input is not None: + zone = {"zone": self.active_cfg} + zone.update(user_input) + self.new_opt[CONF_SENSORS] = self.new_opt.get(CONF_SENSORS, []) + [zone] + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None + + if self.active_cfg: + current_cfg = self.get_current_cfg(CONF_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_digital", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht") + ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])), + vol.Optional( + CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_POLL_INTERVAL, + default=current_cfg.get(CONF_POLL_INTERVAL, 3), + ): vol.All(vol.Coerce(int), vol.Range(min=1)), + } + ), + description_placeholders={ + "zone": "Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + # find the next unconfigured digital sensor + for key, value in self.io_cfg.items(): + if value == CONF_IO_DIG: + self.active_cfg = key + current_cfg = self.get_current_cfg(CONF_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_digital", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht") + ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])), + vol.Optional( + CONF_NAME, + default=current_cfg.get(CONF_NAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_POLL_INTERVAL, + default=current_cfg.get(CONF_POLL_INTERVAL, 3), + ): vol.All(vol.Coerce(int), vol.Range(min=1)), + } + ), + description_placeholders={ + "zone": "Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + return await self.async_step_options_switch() + + async def async_step_options_switch(self, user_input=None): + """Allow the user to configure the IO options for switches.""" + errors = {} + if user_input is not None: + zone = {"zone": self.active_cfg} + zone.update(user_input) + self.new_opt[CONF_SWITCHES] = self.new_opt.get(CONF_SWITCHES, []) + [zone] + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None + + if self.active_cfg: + current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg) + return self.async_show_form( + step_id="options_switch", + data_schema=vol.Schema( + { + vol.Optional( + CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_ACTIVATION, + default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH), + ): vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)), + vol.Optional( + CONF_MOMENTARY, + default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_PAUSE, + default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_REPEAT, + default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=-1)), + } + ), + description_placeholders={ + "zone": "Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + # find the next unconfigured switch + for key, value in self.io_cfg.items(): + if value == CONF_IO_SWI: + self.active_cfg = key + current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg) + return self.async_show_form( + step_id="options_switch", + data_schema=vol.Schema( + { + vol.Optional( + CONF_NAME, + default=current_cfg.get(CONF_NAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_ACTIVATION, + default=current_cfg.get(CONF_ACTIVATION, "high"), + ): vol.In(["low", "high"]), + vol.Optional( + CONF_MOMENTARY, + default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_PAUSE, + default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_REPEAT, + default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=-1)), + } + ), + description_placeholders={ + "zone": "Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + return await self.async_step_options_misc() + + async def async_step_options_misc(self, user_input=None): + """Allow the user to configure the LED behavior.""" + errors = {} + if user_input is not None: + self.new_opt[CONF_BLINK] = user_input[CONF_BLINK] + return self.async_create_entry(title="", data=self.new_opt) + + return self.async_show_form( + step_id="options_misc", + data_schema=vol.Schema( + { + vol.Required( + CONF_BLINK, default=self.current_opt.get(CONF_BLINK, True) + ): bool, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py index 0107b341532..d8777a5611e 100644 --- a/homeassistant/components/konnected/const.py +++ b/homeassistant/components/konnected/const.py @@ -14,11 +14,33 @@ CONF_BLINK = "blink" CONF_DISCOVERY = "discovery" CONF_DHT_SENSORS = "dht_sensors" CONF_DS18B20_SENSORS = "ds18b20_sensors" +CONF_MODEL = "model" STATE_LOW = "low" STATE_HIGH = "high" -PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: "out", 9: 6} +ZONES = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "alarm1", + "out1", + "alarm2_out2", + "out", +] + +# alarm panel pro only handles zones, +# alarm panel allows specifying pins via configuration.yaml +PIN_TO_ZONE = {"1": "1", "2": "2", "5": "3", "6": "4", "7": "5", "8": "out", "9": "6"} ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} ENDPOINT_ROOT = "/api/konnected" diff --git a/homeassistant/components/konnected/errors.py b/homeassistant/components/konnected/errors.py new file mode 100644 index 00000000000..5a0207f3f8d --- /dev/null +++ b/homeassistant/components/konnected/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Konnected component.""" +from homeassistant.exceptions import HomeAssistantError + + +class KonnectedException(HomeAssistantError): + """Base class for Konnected exceptions.""" + + +class CannotConnect(KonnectedException): + """Unable to connect to the panel.""" diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index feb6a4589cb..3a74e2165df 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -1,8 +1,21 @@ { "domain": "konnected", "name": "Konnected", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/konnected", - "requirements": ["konnected==0.1.5"], - "dependencies": ["http"], - "codeowners": ["@heythisisnate"] + "requirements": [ + "konnected==1.1.0" + ], + "ssdp": [ + { + "manufacturer": "konnected.io" + } + ], + "dependencies": [ + "http" + ], + "codeowners": [ + "@heythisisnate", + "@kit-klein" + ] } diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py new file mode 100644 index 00000000000..9f4b39e82bc --- /dev/null +++ b/homeassistant/components/konnected/panel.py @@ -0,0 +1,359 @@ +"""Support for Konnected devices.""" +import asyncio +import logging + +import konnected + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_STATE, + CONF_ACCESS_TOKEN, + CONF_BINARY_SENSORS, + CONF_DEVICES, + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PIN, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TYPE, + CONF_ZONE, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + CONF_ACTIVATION, + CONF_API_HOST, + CONF_BLINK, + CONF_DHT_SENSORS, + CONF_DISCOVERY, + CONF_DS18B20_SENSORS, + CONF_INVERSE, + CONF_MOMENTARY, + CONF_PAUSE, + CONF_POLL_INTERVAL, + CONF_REPEAT, + DOMAIN, + ENDPOINT_ROOT, + SIGNAL_SENSOR_UPDATE, + STATE_LOW, + ZONE_TO_PIN, +) +from .errors import CannotConnect + +_LOGGER = logging.getLogger(__name__) + +KONN_MODEL = "Konnected" +KONN_MODEL_PRO = "Konnected Pro" + +# Indicate how each unit is controlled (pin or zone) +KONN_API_VERSIONS = { + KONN_MODEL: CONF_PIN, + KONN_MODEL_PRO: CONF_ZONE, +} + + +class AlarmPanel: + """A representation of a Konnected alarm panel.""" + + def __init__(self, hass, config_entry): + """Initialize the Konnected device.""" + self.hass = hass + self.config_entry = config_entry + self.config = config_entry.data + self.options = config_entry.options + self.host = self.config.get(CONF_HOST) + self.port = self.config.get(CONF_PORT) + self.client = None + self.status = None + self.api_version = KONN_API_VERSIONS[KONN_MODEL] + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.config.get(CONF_ID) + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) + + def format_zone(self, zone, other_items=None): + """Get zone or pin based dict based on the client type.""" + payload = { + self.api_version: zone + if self.api_version == CONF_ZONE + else ZONE_TO_PIN[zone] + } + payload.update(other_items or {}) + return payload + + async def async_connect(self): + """Connect to and setup a Konnected device.""" + try: + self.client = konnected.Client( + host=self.host, + port=str(self.port), + websession=aiohttp_client.async_get_clientsession(self.hass), + ) + self.status = await self.client.get_status() + self.api_version = KONN_API_VERSIONS.get( + self.status.get("model", KONN_MODEL), KONN_API_VERSIONS[KONN_MODEL] + ) + _LOGGER.info( + "Connected to new %s device", self.status.get("model", "Konnected") + ) + _LOGGER.debug(self.status) + + await self.async_update_initial_states() + # brief delay to allow processing of recent status req + await asyncio.sleep(0.1) + await self.async_sync_device_config() + + except self.client.ClientError as err: + _LOGGER.warning("Exception trying to connect to panel: %s", err) + raise CannotConnect + + _LOGGER.info( + "Set up Konnected device %s. Open http://%s:%s in a " + "web browser to view device status", + self.device_id, + self.host, + self.port, + ) + + device_registry = await dr.async_get_registry(self.hass) + + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, self.status.get("mac"))}, + identifiers={(DOMAIN, self.device_id)}, + manufacturer="Konnected.io", + name=self.config_entry.title, + model=self.config_entry.title, + sw_version=self.status.get("swVersion"), + ) + + async def update_switch(self, zone, state, momentary=None, times=None, pause=None): + """Update the state of a switchable output.""" + try: + if self.client: + if self.api_version == CONF_ZONE: + return await self.client.put_zone( + zone, state, momentary, times, pause, + ) + + # device endpoint uses pin number instead of zone + return await self.client.put_device( + ZONE_TO_PIN[zone], state, momentary, times, pause, + ) + + except self.client.ClientError as err: + _LOGGER.warning("Exception trying to update panel: %s", err) + + raise CannotConnect + + async def async_save_data(self): + """Save the device configuration to `hass.data`.""" + binary_sensors = {} + for entity in self.options.get(CONF_BINARY_SENSORS) or []: + zone = entity[CONF_ZONE] + + binary_sensors[zone] = { + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get( + CONF_NAME, f"Konnected {self.device_id[6:]} Zone {zone}" + ), + CONF_INVERSE: entity.get(CONF_INVERSE), + ATTR_STATE: None, + } + _LOGGER.debug( + "Set up binary_sensor %s (initial state: %s)", + binary_sensors[zone].get("name"), + binary_sensors[zone].get(ATTR_STATE), + ) + + actuators = [] + for entity in self.options.get(CONF_SWITCHES) or []: + zone = entity[CONF_ZONE] + + act = { + CONF_ZONE: zone, + CONF_NAME: entity.get( + CONF_NAME, f"Konnected {self.device_id[6:]} Actuator {zone}", + ), + ATTR_STATE: None, + CONF_ACTIVATION: entity[CONF_ACTIVATION], + CONF_MOMENTARY: entity.get(CONF_MOMENTARY), + CONF_PAUSE: entity.get(CONF_PAUSE), + CONF_REPEAT: entity.get(CONF_REPEAT), + } + actuators.append(act) + _LOGGER.debug("Set up switch %s", act) + + sensors = [] + for entity in self.options.get(CONF_SENSORS) or []: + zone = entity[CONF_ZONE] + + sensor = { + CONF_ZONE: zone, + CONF_NAME: entity.get( + CONF_NAME, f"Konnected {self.device_id[6:]} Sensor {zone}" + ), + CONF_TYPE: entity[CONF_TYPE], + CONF_POLL_INTERVAL: entity.get(CONF_POLL_INTERVAL), + } + sensors.append(sensor) + _LOGGER.debug( + "Set up %s sensor %s (initial state: %s)", + sensor.get(CONF_TYPE), + sensor.get(CONF_NAME), + sensor.get(ATTR_STATE), + ) + + device_data = { + CONF_BINARY_SENSORS: binary_sensors, + CONF_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_BLINK: self.options.get(CONF_BLINK), + CONF_DISCOVERY: self.options.get(CONF_DISCOVERY), + CONF_HOST: self.host, + CONF_PORT: self.port, + "panel": self, + } + + if CONF_DEVICES not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][CONF_DEVICES] = {} + + _LOGGER.debug( + "Storing data in hass.data[%s][%s][%s]: %s", + DOMAIN, + CONF_DEVICES, + self.device_id, + device_data, + ) + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + + @callback + def async_binary_sensor_configuration(self): + """Return the configuration map for syncing binary sensors.""" + return [ + self.format_zone(p) for p in self.stored_configuration[CONF_BINARY_SENSORS] + ] + + @callback + def async_actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [ + self.format_zone( + data[CONF_ZONE], + {"trigger": (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] else 1)}, + ) + for data in self.stored_configuration[CONF_SWITCHES] + ] + + @callback + def async_dht_sensor_configuration(self): + """Return the configuration map for syncing DHT sensors.""" + return [ + self.format_zone( + sensor[CONF_ZONE], {CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]} + ) + for sensor in self.stored_configuration[CONF_SENSORS] + if sensor[CONF_TYPE] == "dht" + ] + + @callback + def async_ds18b20_sensor_configuration(self): + """Return the configuration map for syncing DS18B20 sensors.""" + return [ + self.format_zone(sensor[CONF_ZONE]) + for sensor in self.stored_configuration[CONF_SENSORS] + if sensor[CONF_TYPE] == "ds18b20" + ] + + async def async_update_initial_states(self): + """Update the initial state of each sensor from status poll.""" + for sensor_data in self.status.get("sensors"): + sensor_config = self.stored_configuration[CONF_BINARY_SENSORS].get( + sensor_data.get(CONF_ZONE, sensor_data.get(CONF_PIN)), {} + ) + entity_id = sensor_config.get(ATTR_ENTITY_ID) + + state = bool(sensor_data.get(ATTR_STATE)) + if sensor_config.get(CONF_INVERSE): + state = not state + + async_dispatcher_send( + self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state + ) + + @callback + def async_desired_settings_payload(self): + """Return a dict representing the desired device configuration.""" + desired_api_host = ( + self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url + ) + desired_api_endpoint = desired_api_host + ENDPOINT_ROOT + + return { + "sensors": self.async_binary_sensor_configuration(), + "actuators": self.async_actuator_configuration(), + "dht_sensors": self.async_dht_sensor_configuration(), + "ds18b20_sensors": self.async_ds18b20_sensor_configuration(), + "auth_token": self.config.get(CONF_ACCESS_TOKEN), + "endpoint": desired_api_endpoint, + "blink": self.options.get(CONF_BLINK, True), + "discovery": self.options.get(CONF_DISCOVERY, True), + } + + @callback + def async_current_settings_payload(self): + """Return a dict of configuration currently stored on the device.""" + settings = self.status["settings"] + if not settings: + settings = {} + + return { + "sensors": [ + {self.api_version: s[self.api_version]} + for s in self.status.get("sensors") + ], + "actuators": self.status.get("actuators"), + "dht_sensors": self.status.get(CONF_DHT_SENSORS), + "ds18b20_sensors": self.status.get(CONF_DS18B20_SENSORS), + "auth_token": settings.get("token"), + "endpoint": settings.get("endpoint"), + "blink": settings.get(CONF_BLINK), + "discovery": settings.get(CONF_DISCOVERY), + } + + async def async_sync_device_config(self): + """Sync the new zone configuration to the Konnected device if needed.""" + _LOGGER.debug( + "Device %s settings payload: %s", + self.device_id, + self.async_desired_settings_payload(), + ) + if ( + self.async_desired_settings_payload() + != self.async_current_settings_payload() + ): + _LOGGER.info("pushing settings to device %s", self.device_id) + await self.client.put_settings(**self.async_desired_settings_payload()) + + +async def get_status(hass, host, port): + """Get the status of a Konnected Panel.""" + client = konnected.Client( + host, str(port), aiohttp_client.async_get_clientsession(hass) + ) + try: + return await client.get_status() + + except client.ClientError as err: + _LOGGER.error("Exception trying to get panel status: %s", err) + raise CannotConnect diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 7498f2bde1d..d189ac8809a 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -4,9 +4,9 @@ import logging from homeassistant.const import ( CONF_DEVICES, CONF_NAME, - CONF_PIN, CONF_SENSORS, CONF_TYPE, + CONF_ZONE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, @@ -25,13 +25,10 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up sensors attached to a Konnected device.""" - if discovery_info is None: - return - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors attached to a Konnected device from a config entry.""" data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info["device_id"] + device_id = config_entry.data["id"] sensors = [] # Initialize all DHT sensors. @@ -53,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ( s for s in data[CONF_DEVICES][device_id][CONF_SENSORS] - if s[CONF_TYPE] == "ds18b20" and s[CONF_PIN] == attrs.get(CONF_PIN) + if s[CONF_TYPE] == "ds18b20" and s[CONF_ZONE] == attrs.get(CONF_ZONE) ), None, ) @@ -85,10 +82,10 @@ class KonnectedSensor(Entity): self._data = data self._device_id = device_id self._type = sensor_type - self._pin_num = self._data.get(CONF_PIN) + self._zone_num = self._data.get(CONF_ZONE) self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unique_id = addr or "{}-{}-{}".format( - device_id, self._pin_num, sensor_type + device_id, self._zone_num, sensor_type ) # set initial state if known at initialization @@ -99,7 +96,7 @@ class KonnectedSensor(Entity): # set entity name if given self._name = self._data.get(CONF_NAME) if self._name: - self._name += " " + SENSOR_TYPES[sensor_type][0] + self._name += f" {SENSOR_TYPES[sensor_type][0]}" @property def unique_id(self) -> str: @@ -121,6 +118,13 @@ class KonnectedSensor(Entity): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, + } + async def async_added_to_hass(self): """Store entity_id and register state change callback.""" entity_id_key = self._addr or self._type diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json new file mode 100644 index 00000000000..1f27b04d811 --- /dev/null +++ b/homeassistant/components/konnected/strings.json @@ -0,0 +1,101 @@ +{ + "config": { + "title": "Konnected.io", + "step": { + "user": { + "title": "Discover Konnected Device", + "description": "Please enter the host information for your Konnected Panel.", + "data": { + "host": "Konnected device IP address", + "port": "Konnected device port" + } + }, + "confirm": { + "title": "Konnected Device Ready", + "description": "Model: {model}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings." + } + }, + "error": { + "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}" + }, + "abort": { + "unknown": "Unknown error occurred", + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "not_konn_panel": "Not a recognized Konnected.io device" + } + }, + "options": { + "title": "Konnected Alarm Panel Options", + "step": { + "options_io": { + "title": "Configure I/O", + "description": "Discovered a {model} at {host}. Select the base configuration of each I/O below - depending on the I/O it may allow for binary sensors (open/close contacts), digital sensors (dht and ds18b20), or switchable outputs. You'll be able to configure detailed options in the next steps.", + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + } + }, + "options_io_ext": { + "title": "Configure Extended I/O", + "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.", + "data": { + "8": "Zone 8", + "9": "Zone 9", + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "out1": "OUT1", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2" + } + }, + "options_binary": { + "title": "Configure Binary Sensor", + "description": "Please select the options for the binary sensor attached to {zone}", + "data": { + "type": "Binary Sensor Type", + "name": "Name (optional)", + "inverse": "Invert the open/close state" + } + }, + "options_digital": { + "title": "Configure Digital Sensor", + "description": "Please select the options for the digital sensor attached to {zone}", + "data": { + "type": "Sensor Type", + "name": "Name (optional)", + "poll_interval": "Poll Interval (minutes) (optional)" + } + }, + "options_switch": { + "title": "Configure Switchable Output", + "description": "Please select the output options for {zone}", + "data": { + "name": "Name (optional)", + "activation": "Output when on", + "momentary": "Pulse duration (ms) (optional)", + "pause": "Pause between pulses (ms) (optional)", + "repeat": "Times to repeat (-1=infinite) (optional)" + } + }, + "options_misc": { + "title": "Configure Misc", + "description": "Please select the desired behavior for your panel", + "data": { + "blink": "Blink panel LED on when sending state change" + } + } + }, + "error": {}, + "abort": { + "not_konn_panel": "Not a recognized Konnected.io device" + } + } +} diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index a88281826c0..d16051eb8da 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -5,12 +5,12 @@ from homeassistant.const import ( ATTR_STATE, CONF_DEVICES, CONF_NAME, - CONF_PIN, CONF_SWITCHES, + CONF_ZONE, ) from homeassistant.helpers.entity import ToggleEntity -from . import ( +from .const import ( CONF_ACTIVATION, CONF_MOMENTARY, CONF_PAUSE, @@ -23,16 +23,13 @@ from . import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set switches attached to a Konnected device.""" - if discovery_info is None: - return - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up switches attached to a Konnected device from a config entry.""" data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info["device_id"] + device_id = config_entry.data["id"] switches = [ - KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data) - for pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES] + KonnectedSwitch(device_id, zone_data.get(CONF_ZONE), zone_data) + for zone_data in data[CONF_DEVICES][device_id][CONF_SWITCHES] ] async_add_entities(switches) @@ -40,11 +37,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KonnectedSwitch(ToggleEntity): """Representation of a Konnected switch.""" - def __init__(self, device_id, pin_num, data): + def __init__(self, device_id, zone_num, data): """Initialize the Konnected switch.""" self._data = data self._device_id = device_id - self._pin_num = pin_num + self._zone_num = zone_num self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) self._momentary = self._data.get(CONF_MOMENTARY) self._pause = self._data.get(CONF_PAUSE) @@ -52,7 +49,7 @@ class KonnectedSwitch(ToggleEntity): self._state = self._boolean_state(self._data.get(ATTR_STATE)) self._name = self._data.get(CONF_NAME) self._unique_id = "{}-{}-{}-{}-{}".format( - device_id, self._pin_num, self._momentary, self._pause, self._repeat + device_id, self._zone_num, self._momentary, self._pause, self._repeat ) @property @@ -71,16 +68,22 @@ class KonnectedSwitch(ToggleEntity): return self._state @property - def client(self): + def panel(self): """Return the Konnected HTTP client.""" - return self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id].get( - "client" - ) + device_data = self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id] + return device_data.get("panel") - def turn_on(self, **kwargs): + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, + } + + async def async_turn_on(self, **kwargs): """Send a command to turn on the switch.""" - resp = self.client.put_device( - self._pin_num, + resp = await self.panel.update_switch( + self._zone_num, int(self._activation == STATE_HIGH), self._momentary, self._repeat, @@ -94,9 +97,11 @@ class KonnectedSwitch(ToggleEntity): # Immediately set the state back off for momentary switches self._set_state(False) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send a command to turn off the switch.""" - resp = self.client.put_device(self._pin_num, int(self._activation == STATE_LOW)) + resp = await self.panel.update_switch( + self._zone_num, int(self._activation == STATE_LOW) + ) if resp.get(ATTR_STATE) is not None: self._set_state(self._boolean_state(resp.get(ATTR_STATE))) @@ -111,9 +116,9 @@ class KonnectedSwitch(ToggleEntity): def _set_state(self, state): self._state = state - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() _LOGGER.debug( - "Setting status of %s actuator pin %s to %s", + "Setting status of %s actuator zone %s to %s", self._device_id, self.name, state, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8b6c0e77585..4c5449d5b2a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -47,6 +47,7 @@ FLOWS = [ "ipma", "iqvia", "izone", + "konnected", "life360", "lifx", "linky", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index bea04484b11..0eb9af0231d 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -36,6 +36,11 @@ SSDP = { "modelName": "Philips hue bridge 2015" } ], + "konnected": [ + { + "manufacturer": "konnected.io" + } + ], "samsungtv": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" diff --git a/requirements_all.txt b/requirements_all.txt index 22732005d35..70d87011a71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,7 +767,7 @@ keyrings.alt==3.4.0 kiwiki-client==0.1.1 # homeassistant.components.konnected -konnected==0.1.5 +konnected==1.1.0 # homeassistant.components.eufy lakeside==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8aa12234896..81c3021e2e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -287,6 +287,9 @@ keyring==20.0.0 # homeassistant.scripts.keyring keyrings.alt==3.4.0 +# homeassistant.components.konnected +konnected==1.1.0 + # homeassistant.components.dyson libpurecool==0.6.1 diff --git a/tests/components/konnected/__init__.py b/tests/components/konnected/__init__.py new file mode 100644 index 00000000000..c5de5224a5d --- /dev/null +++ b/tests/components/konnected/__init__.py @@ -0,0 +1 @@ +"""Tests for the Konnected component.""" diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py new file mode 100644 index 00000000000..9b7a498731d --- /dev/null +++ b/tests/components/konnected/test_config_flow.py @@ -0,0 +1,1052 @@ +"""Tests for Konnected Alarm Panel config flow.""" +from asynctest import patch +import pytest + +from homeassistant.components import konnected +from homeassistant.components.konnected import config_flow + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_panel") +async def mock_panel_fixture(): + """Mock a Konnected Panel bridge.""" + with patch("konnected.Client", autospec=True) as konn_client: + + def mock_constructor(host, port, websession): + """Fake the panel constructor.""" + konn_client.host = host + konn_client.port = port + return konn_client + + konn_client.side_effect = mock_constructor + konn_client.ClientError = config_flow.CannotConnect + yield konn_client + + +async def test_flow_works(hass, mock_panel): + """Test config flow .""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected", + } + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"} + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + "model": "Konnected Alarm Panel", + "host": "1.2.3.4", + "port": 1234, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + assert result["data"]["host"] == "1.2.3.4" + assert result["data"]["port"] == 1234 + assert result["data"]["model"] == "Konnected" + assert len(result["data"]["access_token"]) == 20 # confirm generated token size + assert result["data"]["default_options"] == config_flow.OPTIONS_SCHEMA( + {config_flow.CONF_IO: {}} + ) + + +async def test_pro_flow_works(hass, mock_panel): + """Test config flow .""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"} + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + "model": "Konnected Alarm Panel Pro", + "host": "1.2.3.4", + "port": 1234, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + assert result["data"]["host"] == "1.2.3.4" + assert result["data"]["port"] == 1234 + assert result["data"]["model"] == "Konnected Pro" + assert len(result["data"]["access_token"]) == 20 # confirm generated token size + assert result["data"]["default_options"] == config_flow.OPTIONS_SCHEMA( + {config_flow.CONF_IO: {}} + ) + + +async def test_ssdp(hass, mock_panel): + """Test a panel being discovered.""" + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected", + } + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "ssdp"}, + data={ + "ssdp_location": "http://1.2.3.4:1234/Device.xml", + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + "model": "Konnected Alarm Panel", + "host": "1.2.3.4", + "port": 1234, + } + + +async def test_import_no_host_user_finish(hass, mock_panel): + """Test importing a panel with no host info.""" + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={ + "default_options": { + "blink": True, + "discovery": True, + "io": { + "1": "Disabled", + "10": "Disabled", + "11": "Disabled", + "12": "Disabled", + "2": "Disabled", + "3": "Disabled", + "4": "Disabled", + "5": "Disabled", + "6": "Disabled", + "7": "Disabled", + "8": "Disabled", + "9": "Disabled", + "alarm1": "Disabled", + "alarm2_out2": "Disabled", + "out": "Disabled", + "out1": "Disabled", + }, + }, + "id": "aabbccddeeff", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + # confirm user is prompted to enter host + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"host": "1.1.1.1", "port": 1234} + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + "model": "Konnected Alarm Panel Pro", + "host": "1.1.1.1", + "port": 1234, + } + + # final confirmation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + + +async def test_ssdp_already_configured(hass, mock_panel): + """Test if a discovered panel has already been configured.""" + MockConfigEntry( + domain="konnected", + data={"host": "0.0.0.0", "port": 1234}, + unique_id="112233445566", + ).add_to_hass(hass) + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "ssdp"}, + data={ + "ssdp_location": "http://0.0.0.0:1234/Device.xml", + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL_PRO, + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_ssdp_host_update(hass, mock_panel): + """Test if a discovered panel has already been configured but changed host.""" + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "11223344556677889900", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "2": "Binary Sensor", + "6": "Binary Sensor", + "10": "Binary Sensor", + "3": "Digital Sensor", + "7": "Digital Sensor", + "11": "Digital Sensor", + "4": "Switchable Output", + "out1": "Switchable Output", + "alarm1": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "2", "type": "door"}, + {"zone": "6", "type": "window", "name": "winder", "inverse": True}, + {"zone": "10", "type": "door"}, + ], + "sensors": [ + {"zone": "3", "type": "dht"}, + {"zone": "7", "type": "ds18b20", "name": "temper"}, + {"zone": "11", "type": "dht"}, + ], + "switches": [ + {"zone": "4"}, + { + "zone": "8", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "out1"}, + {"zone": "alarm1"}, + ], + } + ) + + MockConfigEntry( + domain="konnected", + data=device_config, + options=device_options, + unique_id="112233445566", + ).add_to_hass(hass) + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "ssdp"}, + data={ + "ssdp_location": "http://1.1.1.1:1234/Device.xml", + "manufacturer": config_flow.KONN_MANUFACTURER, + "modelName": config_flow.KONN_MODEL_PRO, + }, + ) + assert result["type"] == "abort" + + # confirm the host value was updated + entry = hass.config_entries.async_entries(config_flow.DOMAIN)[0] + assert entry.data["host"] == "1.1.1.1" + assert entry.data["port"] == 1234 + + +async def test_import_existing_config(hass, mock_panel): + """Test importing a host with an existing config file.""" + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data=konnected.DEVICE_SCHEMA_YAML( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "binary_sensors": [ + {"zone": "2", "type": "door"}, + {"zone": 6, "type": "window", "name": "winder", "inverse": True}, + {"zone": "10", "type": "door"}, + ], + "sensors": [ + {"zone": "3", "type": "dht"}, + {"zone": 7, "type": "ds18b20", "name": "temper"}, + {"zone": "11", "type": "dht"}, + ], + "switches": [ + {"zone": "4"}, + { + "zone": 8, + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "out1"}, + {"zone": "alarm1"}, + ], + } + ), + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + assert result["data"] == { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": result["data"]["access_token"], + "default_options": { + "io": { + "1": "Disabled", + "5": "Disabled", + "9": "Disabled", + "12": "Disabled", + "out": "Disabled", + "alarm2_out2": "Disabled", + "2": "Binary Sensor", + "6": "Binary Sensor", + "10": "Binary Sensor", + "3": "Digital Sensor", + "7": "Digital Sensor", + "11": "Digital Sensor", + "4": "Switchable Output", + "8": "Switchable Output", + "out1": "Switchable Output", + "alarm1": "Switchable Output", + }, + "blink": True, + "discovery": True, + "binary_sensors": [ + {"zone": "2", "type": "door", "inverse": False}, + {"zone": "6", "type": "window", "name": "winder", "inverse": True}, + {"zone": "10", "type": "door", "inverse": False}, + ], + "sensors": [ + {"zone": "3", "type": "dht", "poll_interval": 3}, + {"zone": "7", "type": "ds18b20", "name": "temper", "poll_interval": 3}, + {"zone": "11", "type": "dht", "poll_interval": 3}, + ], + "switches": [ + {"activation": "high", "zone": "4"}, + { + "zone": "8", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"activation": "high", "zone": "out1"}, + {"activation": "high", "zone": "alarm1"}, + ], + }, + } + + +async def test_import_existing_config_entry(hass, mock_panel): + """Test importing a host that has an existing config entry.""" + MockConfigEntry( + domain="konnected", + data={ + "host": "0.0.0.0", + "port": 1111, + "id": "112233445566", + "extra": "something", + }, + unique_id="112233445566", + ).add_to_hass(hass) + + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + + # utilize a global access token this time + hass.data[config_flow.DOMAIN] = {"access_token": "SUPERSECRETTOKEN"} + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={ + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "default_options": { + "blink": True, + "discovery": True, + "io": { + "1": "Disabled", + "10": "Binary Sensor", + "11": "Disabled", + "12": "Disabled", + "2": "Binary Sensor", + "3": "Disabled", + "4": "Disabled", + "5": "Disabled", + "6": "Binary Sensor", + "7": "Disabled", + "8": "Disabled", + "9": "Disabled", + "alarm1": "Disabled", + "alarm2_out2": "Disabled", + "out": "Disabled", + "out1": "Disabled", + }, + "binary_sensors": [ + {"inverse": False, "type": "door", "zone": "2"}, + {"inverse": True, "type": "Window", "name": "winder", "zone": "6"}, + {"inverse": False, "type": "door", "zone": "10"}, + ], + }, + }, + ) + + assert result["type"] == "abort" + + # We should have updated the entry + assert len(hass.config_entries.async_entries("konnected")) == 1 + assert hass.config_entries.async_entries("konnected")[0].data == { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "SUPERSECRETTOKEN", + "extra": "something", + } + + +async def test_import_pin_config(hass, mock_panel): + """Test importing a host with an existing config file that specifies pin configs.""" + mock_panel.get_status.return_value = { + "mac": "11:22:33:44:55:66", + "name": "Konnected Pro", + } + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data=konnected.DEVICE_SCHEMA_YAML( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "binary_sensors": [ + {"pin": 1, "type": "door"}, + {"pin": "2", "type": "window", "name": "winder", "inverse": True}, + {"zone": "3", "type": "door"}, + ], + "sensors": [ + {"zone": 4, "type": "dht"}, + {"pin": "7", "type": "ds18b20", "name": "temper"}, + ], + "switches": [ + { + "pin": "8", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "6"}, + ], + } + ), + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + assert result["data"] == { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": result["data"]["access_token"], + "default_options": { + "io": { + "7": "Disabled", + "8": "Disabled", + "9": "Disabled", + "10": "Disabled", + "11": "Disabled", + "12": "Disabled", + "out1": "Disabled", + "alarm1": "Disabled", + "alarm2_out2": "Disabled", + "1": "Binary Sensor", + "2": "Binary Sensor", + "3": "Binary Sensor", + "4": "Digital Sensor", + "5": "Digital Sensor", + "6": "Switchable Output", + "out": "Switchable Output", + }, + "blink": True, + "discovery": True, + "binary_sensors": [ + {"zone": "1", "type": "door", "inverse": False}, + {"zone": "2", "type": "window", "name": "winder", "inverse": True}, + {"zone": "3", "type": "door", "inverse": False}, + ], + "sensors": [ + {"zone": "4", "type": "dht", "poll_interval": 3}, + {"zone": "5", "type": "ds18b20", "name": "temper", "poll_interval": 3}, + ], + "switches": [ + { + "zone": "out", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"activation": "high", "zone": "6"}, + ], + }, + } + + +async def test_option_flow(hass, mock_panel): + """Test config flow options.""" + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected", + "access_token": "11223344556677889900", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA({"io": {}}) + + entry = MockConfigEntry( + domain="konnected", + data=device_config, + options=device_options, + unique_id="112233445566", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"source": "test"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_io" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "1": "Disabled", + "2": "Binary Sensor", + "3": "Digital Sensor", + "4": "Switchable Output", + "5": "Disabled", + "6": "Binary Sensor", + "out": "Switchable Output", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_binary" + + # zone 2 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "door"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_binary" + + # zone 6 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"type": "window", "name": "winder", "inverse": True}, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_digital" + + # zone 3 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "dht"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone 4 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone out + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "options_misc" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"blink": True}, + ) + assert result["type"] == "create_entry" + assert result["data"] == { + "io": { + "2": "Binary Sensor", + "3": "Digital Sensor", + "4": "Switchable Output", + "6": "Binary Sensor", + "out": "Switchable Output", + }, + "blink": True, + "binary_sensors": [ + {"zone": "2", "type": "door", "inverse": False}, + {"zone": "6", "type": "window", "name": "winder", "inverse": True}, + ], + "sensors": [{"zone": "3", "type": "dht", "poll_interval": 3}], + "switches": [ + {"activation": "high", "zone": "4"}, + { + "zone": "out", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + ], + } + + +async def test_option_flow_pro(hass, mock_panel): + """Test config flow options for pro board.""" + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "11223344556677889900", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA({"io": {}}) + + entry = MockConfigEntry( + domain="konnected", + data=device_config, + options=device_options, + unique_id="112233445566", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"source": "test"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_io" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "1": "Disabled", + "2": "Binary Sensor", + "3": "Digital Sensor", + "4": "Switchable Output", + "5": "Disabled", + "6": "Binary Sensor", + "7": "Digital Sensor", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_io_ext" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "8": "Switchable Output", + "9": "Disabled", + "10": "Binary Sensor", + "11": "Digital Sensor", + "12": "Disabled", + "out1": "Switchable Output", + "alarm1": "Switchable Output", + "alarm2_out2": "Disabled", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_binary" + + # zone 2 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "door"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_binary" + + # zone 6 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"type": "window", "name": "winder", "inverse": True}, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_binary" + + # zone 10 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "door"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_digital" + + # zone 3 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "dht"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_digital" + + # zone 7 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "ds18b20", "name": "temper"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_digital" + + # zone 11 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "dht"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone 4 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone 8 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone out1 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone alarm1 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_misc" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"blink": True}, + ) + + assert result["type"] == "create_entry" + assert result["data"] == { + "io": { + "10": "Binary Sensor", + "11": "Digital Sensor", + "2": "Binary Sensor", + "3": "Digital Sensor", + "4": "Switchable Output", + "6": "Binary Sensor", + "7": "Digital Sensor", + "8": "Switchable Output", + "alarm1": "Switchable Output", + "out1": "Switchable Output", + }, + "blink": True, + "binary_sensors": [ + {"zone": "2", "type": "door", "inverse": False}, + {"zone": "6", "type": "window", "name": "winder", "inverse": True}, + {"zone": "10", "type": "door", "inverse": False}, + ], + "sensors": [ + {"zone": "3", "type": "dht", "poll_interval": 3}, + {"zone": "7", "type": "ds18b20", "name": "temper", "poll_interval": 3}, + {"zone": "11", "type": "dht", "poll_interval": 3}, + ], + "switches": [ + {"activation": "high", "zone": "4"}, + { + "zone": "8", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"activation": "high", "zone": "out1"}, + {"activation": "high", "zone": "alarm1"}, + ], + } + + +async def test_option_flow_import(hass, mock_panel): + """Test config flow options imported from configuration.yaml.""" + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "1": "Binary Sensor", + "2": "Digital Sensor", + "3": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "1", "type": "window", "name": "winder", "inverse": True}, + ], + "sensors": [{"zone": "2", "type": "ds18b20", "name": "temper"}], + "switches": [ + { + "zone": "3", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + ], + } + ) + + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "11223344556677889900", + "default_options": device_options, + } + ) + + entry = MockConfigEntry( + domain="konnected", data=device_config, unique_id="112233445566" + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"source": "test"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_io" + + # confirm the defaults are set based on current config - we"ll spot check this throughout + schema = result["data_schema"]({}) + assert schema["1"] == "Binary Sensor" + assert schema["2"] == "Digital Sensor" + assert schema["3"] == "Switchable Output" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "1": "Binary Sensor", + "2": "Digital Sensor", + "3": "Switchable Output", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_io_ext" + schema = result["data_schema"]({}) + assert schema["8"] == "Disabled" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={}, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_binary" + + # zone 1 + schema = result["data_schema"]({}) + assert schema["type"] == "window" + assert schema["name"] == "winder" + assert schema["inverse"] is True + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "door"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_digital" + + # zone 2 + schema = result["data_schema"]({}) + assert schema["type"] == "ds18b20" + assert schema["name"] == "temper" + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"type": "dht"}, + ) + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + + # zone 3 + schema = result["data_schema"]({}) + assert schema["name"] == "switcher" + assert schema["activation"] == "low" + assert schema["momentary"] == 50 + assert schema["pause"] == 100 + assert schema["repeat"] == 4 + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"activation": "high"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_misc" + + schema = result["data_schema"]({}) + assert schema["blink"] is True + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"blink": False}, + ) + + # verify the updated fields + assert result["type"] == "create_entry" + assert result["data"] == { + "io": {"1": "Binary Sensor", "2": "Digital Sensor", "3": "Switchable Output"}, + "blink": False, + "binary_sensors": [ + {"zone": "1", "type": "door", "inverse": True, "name": "winder"}, + ], + "sensors": [ + {"zone": "2", "type": "dht", "poll_interval": 3, "name": "temper"}, + ], + "switches": [ + { + "zone": "3", + "name": "switcher", + "activation": "high", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + ], + } + + +async def test_option_flow_existing(hass, mock_panel): + """Test config flow options with existing already in place.""" + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "1": "Binary Sensor", + "2": "Digital Sensor", + "3": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "1", "type": "window", "name": "winder", "inverse": True}, + ], + "sensors": [{"zone": "2", "type": "ds18b20", "name": "temper"}], + "switches": [ + { + "zone": "3", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + ], + } + ) + + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "11223344556677889900", + "default_options": config_flow.OPTIONS_SCHEMA({"io": {}}), + } + ) + + entry = MockConfigEntry( + domain="konnected", + data=device_config, + options=device_options, + unique_id="112233445566", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"source": "test"} + ) + assert result["type"] == "form" + assert result["step_id"] == "options_io" + + # confirm the defaults are pulled in from the existing options + schema = result["data_schema"]({}) + assert schema["1"] == "Binary Sensor" + assert schema["2"] == "Digital Sensor" + assert schema["3"] == "Switchable Output" diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py new file mode 100644 index 00000000000..e1a1d2e72f8 --- /dev/null +++ b/tests/components/konnected/test_init.py @@ -0,0 +1,601 @@ +"""Test Konnected setup process.""" +from asynctest import patch +import pytest + +from homeassistant.components import konnected +from homeassistant.components.konnected import config_flow +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_panel") +async def mock_panel_fixture(): + """Mock a Konnected Panel bridge.""" + with patch("konnected.Client", autospec=True) as konn_client: + + def mock_constructor(host, port, websession): + """Fake the panel constructor.""" + konn_client.host = host + konn_client.port = port + return konn_client + + konn_client.side_effect = mock_constructor + konn_client.ClientError = config_flow.CannotConnect + konn_client.get_status.return_value = { + "hwVersion": "2.3.0", + "swVersion": "2.3.1", + "heap": 10000, + "uptime": 12222, + "ip": "192.168.1.90", + "port": 9123, + "sensors": [], + "actuators": [], + "dht_sensors": [], + "ds18b20_sensors": [], + "mac": "11:22:33:44:55:66", + "settings": {}, + } + yield konn_client + + +async def test_config_schema(hass): + """Test that config schema is imported properly.""" + config = { + konnected.DOMAIN: { + konnected.CONF_ACCESS_TOKEN: "abcdefgh", + konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}], + } + } + assert konnected.CONFIG_SCHEMA(config) == { + "konnected": { + "access_token": "abcdefgh", + "devices": [ + { + "default_options": { + "blink": True, + "discovery": True, + "io": { + "1": "Disabled", + "10": "Disabled", + "11": "Disabled", + "12": "Disabled", + "2": "Disabled", + "3": "Disabled", + "4": "Disabled", + "5": "Disabled", + "6": "Disabled", + "7": "Disabled", + "8": "Disabled", + "9": "Disabled", + "alarm1": "Disabled", + "alarm2_out2": "Disabled", + "out": "Disabled", + "out1": "Disabled", + }, + }, + "id": "aabbccddeeff", + } + ], + } + } + + # check with host info + config = { + konnected.DOMAIN: { + konnected.CONF_ACCESS_TOKEN: "abcdefgh", + konnected.CONF_DEVICES: [ + {konnected.CONF_ID: "aabbccddeeff", "host": "192.168.1.1", "port": 1234} + ], + } + } + assert konnected.CONFIG_SCHEMA(config) == { + "konnected": { + "access_token": "abcdefgh", + "devices": [ + { + "default_options": { + "blink": True, + "discovery": True, + "io": { + "1": "Disabled", + "10": "Disabled", + "11": "Disabled", + "12": "Disabled", + "2": "Disabled", + "3": "Disabled", + "4": "Disabled", + "5": "Disabled", + "6": "Disabled", + "7": "Disabled", + "8": "Disabled", + "9": "Disabled", + "alarm1": "Disabled", + "alarm2_out2": "Disabled", + "out": "Disabled", + "out1": "Disabled", + }, + }, + "id": "aabbccddeeff", + "host": "192.168.1.1", + "port": 1234, + } + ], + } + } + + # check pin to zone + config = { + konnected.DOMAIN: { + konnected.CONF_ACCESS_TOKEN: "abcdefgh", + konnected.CONF_DEVICES: [ + { + konnected.CONF_ID: "aabbccddeeff", + "binary_sensors": [ + {"pin": 2, "type": "door"}, + {"zone": 1, "type": "door"}, + ], + } + ], + } + } + assert konnected.CONFIG_SCHEMA(config) == { + "konnected": { + "access_token": "abcdefgh", + "devices": [ + { + "default_options": { + "blink": True, + "discovery": True, + "io": { + "1": "Binary Sensor", + "10": "Disabled", + "11": "Disabled", + "12": "Disabled", + "2": "Binary Sensor", + "3": "Disabled", + "4": "Disabled", + "5": "Disabled", + "6": "Disabled", + "7": "Disabled", + "8": "Disabled", + "9": "Disabled", + "alarm1": "Disabled", + "alarm2_out2": "Disabled", + "out": "Disabled", + "out1": "Disabled", + }, + "binary_sensors": [ + {"inverse": False, "type": "door", "zone": "2"}, + {"inverse": False, "type": "door", "zone": "1"}, + ], + }, + "id": "aabbccddeeff", + } + ], + } + } + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a Konnected panel.""" + assert await async_setup_component(hass, konnected.DOMAIN, {}) + + # No flows started + assert len(hass.config_entries.flow.async_progress()) == 0 + + # Nothing saved from configuration.yaml + assert hass.data[konnected.DOMAIN][konnected.CONF_ACCESS_TOKEN] is None + assert hass.data[konnected.DOMAIN][konnected.CONF_API_HOST] is None + assert konnected.YAML_CONFIGS not in hass.data[konnected.DOMAIN] + + +async def test_setup_defined_hosts_known_auth(hass): + """Test we don't initiate a config entry if configured panel is known.""" + MockConfigEntry( + domain="konnected", + unique_id="112233445566", + data={"host": "0.0.0.0", "id": "112233445566"}, + ).add_to_hass(hass) + MockConfigEntry( + domain="konnected", + unique_id="aabbccddeeff", + data={"host": "1.2.3.4", "id": "aabbccddeeff"}, + ).add_to_hass(hass) + + assert ( + await async_setup_component( + hass, + konnected.DOMAIN, + { + konnected.DOMAIN: { + konnected.CONF_ACCESS_TOKEN: "abcdefgh", + konnected.CONF_DEVICES: [ + { + config_flow.CONF_ID: "aabbccddeeff", + config_flow.CONF_HOST: "0.0.0.0", + config_flow.CONF_PORT: 1234, + }, + ], + } + }, + ) + is True + ) + + assert hass.data[konnected.DOMAIN][konnected.CONF_ACCESS_TOKEN] == "abcdefgh" + assert konnected.YAML_CONFIGS not in hass.data[konnected.DOMAIN] + + # Flow aborted + assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_setup_defined_hosts_no_known_auth(hass): + """Test we initiate config entry if config panel is not known.""" + assert ( + await async_setup_component( + hass, + konnected.DOMAIN, + { + konnected.DOMAIN: { + konnected.CONF_ACCESS_TOKEN: "abcdefgh", + konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}], + } + }, + ) + is True + ) + + # Flow started for discovered bridge + assert len(hass.config_entries.flow.async_progress()) == 1 + + +async def test_config_passed_to_config_entry(hass): + """Test that configured options for a host are loaded via config entry.""" + entry = MockConfigEntry( + domain=konnected.DOMAIN, + data={config_flow.CONF_ID: "aabbccddeeff", config_flow.CONF_HOST: "0.0.0.0"}, + ) + entry.add_to_hass(hass) + with patch.object(konnected, "AlarmPanel", autospec=True) as mock_int: + assert ( + await async_setup_component( + hass, + konnected.DOMAIN, + { + konnected.DOMAIN: { + konnected.CONF_ACCESS_TOKEN: "abcdefgh", + konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}], + } + }, + ) + is True + ) + + assert len(mock_int.mock_calls) == 3 + p_hass, p_entry = mock_int.mock_calls[0][1] + + assert p_hass is hass + assert p_entry is entry + + +async def test_unload_entry(hass, mock_panel): + """Test being able to unload an entry.""" + entry = MockConfigEntry( + domain=konnected.DOMAIN, data={konnected.CONF_ID: "aabbccddeeff"} + ) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, konnected.DOMAIN, {}) is True + assert hass.data[konnected.DOMAIN]["devices"].get("aabbccddeeff") is not None + assert await konnected.async_unload_entry(hass, entry) + assert hass.data[konnected.DOMAIN]["devices"] == {} + + +async def test_api(hass, aiohttp_client, mock_panel): + """Test callback view.""" + await async_setup_component(hass, "http", {"http": {}}) + + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "abcdefgh", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "1": "Binary Sensor", + "2": "Binary Sensor", + "3": "Binary Sensor", + "4": "Digital Sensor", + "5": "Digital Sensor", + "6": "Switchable Output", + "out": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "1", "type": "door"}, + {"zone": "2", "type": "window", "name": "winder", "inverse": True}, + {"zone": "3", "type": "door"}, + ], + "sensors": [ + {"zone": "4", "type": "dht"}, + {"zone": "5", "type": "ds18b20", "name": "temper"}, + ], + "switches": [ + { + "zone": "out", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "6"}, + ], + } + ) + + entry = MockConfigEntry( + domain="konnected", + title="Konnected Alarm Panel", + data=device_config, + options=device_options, + ) + entry.add_to_hass(hass) + + assert ( + await async_setup_component( + hass, + konnected.DOMAIN, + {konnected.DOMAIN: {konnected.CONF_ACCESS_TOKEN: "globaltoken"}}, + ) + is True + ) + + client = await aiohttp_client(hass.http.app) + + # Test the get endpoint for switch status polling + resp = await client.get("/api/konnected") + assert resp.status == 404 # no device provided + + resp = await client.get("/api/konnected/223344556677") + assert resp.status == 404 # unknown device provided + + resp = await client.get("/api/konnected/device/112233445566") + assert resp.status == 404 # no zone provided + result = await resp.json() + assert result == {"message": "Switch on zone or pin unknown not configured"} + + resp = await client.get("/api/konnected/device/112233445566?zone=8") + assert resp.status == 404 # invalid zone + result = await resp.json() + assert result == {"message": "Switch on zone or pin 8 not configured"} + + resp = await client.get("/api/konnected/device/112233445566?pin=12") + assert resp.status == 404 # invalid pin + result = await resp.json() + assert result == {"message": "Switch on zone or pin 12 not configured"} + + resp = await client.get("/api/konnected/device/112233445566?zone=out") + assert resp.status == 200 + result = await resp.json() + assert result == {"state": 1, "zone": "out"} + + resp = await client.get("/api/konnected/device/112233445566?pin=8") + assert resp.status == 200 + result = await resp.json() + assert result == {"state": 1, "pin": "8"} + + # Test the post endpoint for sensor updates + resp = await client.post("/api/konnected/device", json={"zone": "1", "state": 1}) + assert resp.status == 404 + + resp = await client.post( + "/api/konnected/device/112233445566", json={"zone": "1", "state": 1} + ) + assert resp.status == 401 + result = await resp.json() + assert result == {"message": "unauthorized"} + + resp = await client.post( + "/api/konnected/device/223344556677", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "1", "state": 1}, + ) + assert resp.status == 400 + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "15", "state": 1}, + ) + assert resp.status == 400 + result = await resp.json() + assert result == {"message": "unregistered sensor/actuator"} + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "1", "state": 1}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer globaltoken"}, + json={"zone": "1", "state": 1}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "4", "temp": 22, "humi": 20}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + + # Test the put endpoint for sensor updates + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "1", "state": 1}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + + +async def test_state_updates(hass, aiohttp_client, mock_panel): + """Test callback view.""" + await async_setup_component(hass, "http", {"http": {}}) + + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "abcdefgh", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "1": "Binary Sensor", + "2": "Binary Sensor", + "3": "Binary Sensor", + "4": "Digital Sensor", + "5": "Digital Sensor", + "6": "Switchable Output", + "out": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "1", "type": "door"}, + {"zone": "2", "type": "window", "name": "winder", "inverse": True}, + {"zone": "3", "type": "door"}, + ], + "sensors": [ + {"zone": "4", "type": "dht"}, + {"zone": "5", "type": "ds18b20", "name": "temper"}, + ], + "switches": [ + { + "zone": "out", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "6"}, + ], + } + ) + + entry = MockConfigEntry( + domain="konnected", + title="Konnected Alarm Panel", + data=device_config, + options=device_options, + ) + entry.add_to_hass(hass) + + assert ( + await async_setup_component( + hass, + konnected.DOMAIN, + {konnected.DOMAIN: {konnected.CONF_ACCESS_TOKEN: "1122334455"}}, + ) + is True + ) + + client = await aiohttp_client(hass.http.app) + + # Test updating a binary sensor + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "1", "state": 0}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.konnected_445566_zone_1").state == "off" + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "1", "state": 1}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.konnected_445566_zone_1").state == "on" + + # Test updating sht sensor + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "4", "temp": 22, "humi": 20}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + await hass.async_block_till_done() + assert hass.states.get("sensor.konnected_445566_sensor_4_humidity").state == "20" + assert ( + hass.states.get("sensor.konnected_445566_sensor_4_temperature").state == "22.0" + ) + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "4", "temp": 25, "humi": 23}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + await hass.async_block_till_done() + assert hass.states.get("sensor.konnected_445566_sensor_4_humidity").state == "23" + assert ( + hass.states.get("sensor.konnected_445566_sensor_4_temperature").state == "25.0" + ) + + # Test updating ds sensor + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "5", "temp": 32, "addr": 1}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + await hass.async_block_till_done() + assert hass.states.get("sensor.temper_temperature").state == "32.0" + + resp = await client.post( + "/api/konnected/device/112233445566", + headers={"Authorization": "Bearer abcdefgh"}, + json={"zone": "5", "temp": 42, "addr": 1}, + ) + assert resp.status == 200 + result = await resp.json() + assert result == {"message": "ok"} + await hass.async_block_till_done() + assert hass.states.get("sensor.temper_temperature").state == "42.0" diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py new file mode 100644 index 00000000000..0ad384bd35e --- /dev/null +++ b/tests/components/konnected/test_panel.py @@ -0,0 +1,375 @@ +"""Test Konnected setup process.""" +from asynctest import patch +import pytest + +from homeassistant.components.konnected import config_flow, panel + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_panel") +async def mock_panel_fixture(): + """Mock a Konnected Panel bridge.""" + with patch("konnected.Client", autospec=True) as konn_client: + + def mock_constructor(host, port, websession): + """Fake the panel constructor.""" + konn_client.host = host + konn_client.port = port + return konn_client + + konn_client.side_effect = mock_constructor + konn_client.ClientError = config_flow.CannotConnect + konn_client.get_status.return_value = { + "hwVersion": "2.3.0", + "swVersion": "2.3.1", + "heap": 10000, + "uptime": 12222, + "ip": "192.168.1.90", + "port": 9123, + "sensors": [], + "actuators": [], + "dht_sensors": [], + "ds18b20_sensors": [], + "mac": "11:22:33:44:55:66", + "model": "Konnected Pro", # `model` field only included in pro + "settings": {}, + } + yield konn_client + + +async def test_create_and_setup(hass, mock_panel): + """Test that we create a Konnected Panel and save the data.""" + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "11223344556677889900", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "1": "Binary Sensor", + "2": "Binary Sensor", + "3": "Binary Sensor", + "4": "Digital Sensor", + "5": "Digital Sensor", + "6": "Switchable Output", + "out": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "1", "type": "door"}, + {"zone": "2", "type": "window", "name": "winder", "inverse": True}, + {"zone": "3", "type": "door"}, + ], + "sensors": [ + {"zone": "4", "type": "dht"}, + {"zone": "5", "type": "ds18b20", "name": "temper"}, + ], + "switches": [ + { + "zone": "out", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "6"}, + ], + } + ) + + entry = MockConfigEntry( + domain="konnected", + title="Konnected Alarm Panel", + data=device_config, + options=device_options, + ) + entry.add_to_hass(hass) + hass.data[panel.DOMAIN] = { + panel.CONF_API_HOST: "192.168.1.1", + } + + # override get_status to reflect non-pro board + mock_panel.get_status.return_value = { + "hwVersion": "2.3.0", + "swVersion": "2.3.1", + "heap": 10000, + "uptime": 12222, + "ip": "192.168.1.90", + "port": 9123, + "sensors": [], + "actuators": [], + "dht_sensors": [], + "ds18b20_sensors": [], + "mac": "11:22:33:44:55:66", + "settings": {}, + } + device = panel.AlarmPanel(hass, entry) + await device.async_save_data() + await device.async_connect() + await device.update_switch("1", 0) + + # confirm the correct api is used + # pylint: disable=no-member + assert device.client.put_device.call_count == 1 + assert device.client.put_zone.call_count == 0 + + # confirm the settings are sent to the panel + # pylint: disable=no-member + assert device.client.put_settings.call_args_list[0][1] == { + "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], + "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], + "dht_sensors": [{"poll_interval": 3, "pin": "6"}], + "ds18b20_sensors": [{"pin": "7"}], + "auth_token": "11223344556677889900", + "blink": True, + "discovery": True, + "endpoint": "192.168.1.1/api/konnected", + } + + # confirm the device settings are saved in hass.data + assert hass.data[panel.DOMAIN][panel.CONF_DEVICES] == { + "112233445566": { + "binary_sensors": { + "1": { + "inverse": False, + "name": "Konnected 445566 Zone 1", + "state": None, + "type": "door", + }, + "2": { + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, + "3": { + "inverse": False, + "name": "Konnected 445566 Zone 3", + "state": None, + "type": "door", + }, + }, + "blink": True, + "panel": device, + "discovery": True, + "host": "1.2.3.4", + "port": 1234, + "sensors": [ + { + "name": "Konnected 445566 Sensor 4", + "poll_interval": 3, + "type": "dht", + "zone": "4", + }, + {"name": "temper", "poll_interval": 3, "type": "ds18b20", "zone": "5"}, + ], + "switches": [ + { + "activation": "low", + "momentary": 50, + "name": "switcher", + "pause": 100, + "repeat": 4, + "state": None, + "zone": "out", + }, + { + "activation": "high", + "momentary": None, + "name": "Konnected 445566 Actuator 6", + "pause": None, + "repeat": None, + "state": None, + "zone": "6", + }, + ], + } + } + + +async def test_create_and_setup_pro(hass, mock_panel): + """Test that we create a Konnected Pro Panel and save the data.""" + device_config = config_flow.CONFIG_ENTRY_SCHEMA( + { + "host": "1.2.3.4", + "port": 1234, + "id": "112233445566", + "model": "Konnected Pro", + "access_token": "11223344556677889900", + "default_options": config_flow.OPTIONS_SCHEMA({config_flow.CONF_IO: {}}), + } + ) + + device_options = config_flow.OPTIONS_SCHEMA( + { + "io": { + "2": "Binary Sensor", + "6": "Binary Sensor", + "10": "Binary Sensor", + "3": "Digital Sensor", + "7": "Digital Sensor", + "11": "Digital Sensor", + "4": "Switchable Output", + "8": "Switchable Output", + "out1": "Switchable Output", + "alarm1": "Switchable Output", + }, + "binary_sensors": [ + {"zone": "2", "type": "door"}, + {"zone": "6", "type": "window", "name": "winder", "inverse": True}, + {"zone": "10", "type": "door"}, + ], + "sensors": [ + {"zone": "3", "type": "dht"}, + {"zone": "7", "type": "ds18b20", "name": "temper"}, + {"zone": "11", "type": "dht", "poll_interval": 5}, + ], + "switches": [ + {"zone": "4"}, + { + "zone": "8", + "name": "switcher", + "activation": "low", + "momentary": 50, + "pause": 100, + "repeat": 4, + }, + {"zone": "out1"}, + {"zone": "alarm1"}, + ], + } + ) + + entry = MockConfigEntry( + domain="konnected", + title="Konnected Pro Alarm Panel", + data=device_config, + options=device_options, + ) + entry.add_to_hass(hass) + hass.data[panel.DOMAIN] = { + panel.CONF_API_HOST: "192.168.1.1", + } + + device = panel.AlarmPanel(hass, entry) + await device.async_save_data() + await device.async_connect() + await device.update_switch("2", 1) + + # confirm the correct api is used + # pylint: disable=no-member + assert device.client.put_device.call_count == 0 + assert device.client.put_zone.call_count == 1 + + # confirm the settings are sent to the panel + # pylint: disable=no-member + assert device.client.put_settings.call_args_list[0][1] == { + "sensors": [{"zone": "2"}, {"zone": "6"}, {"zone": "10"}], + "actuators": [ + {"trigger": 1, "zone": "4"}, + {"trigger": 0, "zone": "8"}, + {"trigger": 1, "zone": "out1"}, + {"trigger": 1, "zone": "alarm1"}, + ], + "dht_sensors": [ + {"poll_interval": 3, "zone": "3"}, + {"poll_interval": 5, "zone": "11"}, + ], + "ds18b20_sensors": [{"zone": "7"}], + "auth_token": "11223344556677889900", + "blink": True, + "discovery": True, + "endpoint": "192.168.1.1/api/konnected", + } + + # confirm the device settings are saved in hass.data + assert hass.data[panel.DOMAIN][panel.CONF_DEVICES] == { + "112233445566": { + "binary_sensors": { + "10": { + "inverse": False, + "name": "Konnected 445566 Zone 10", + "state": None, + "type": "door", + }, + "2": { + "inverse": False, + "name": "Konnected 445566 Zone 2", + "state": None, + "type": "door", + }, + "6": { + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, + }, + "blink": True, + "panel": device, + "discovery": True, + "host": "1.2.3.4", + "port": 1234, + "sensors": [ + { + "name": "Konnected 445566 Sensor 3", + "poll_interval": 3, + "type": "dht", + "zone": "3", + }, + {"name": "temper", "poll_interval": 3, "type": "ds18b20", "zone": "7"}, + { + "name": "Konnected 445566 Sensor 11", + "poll_interval": 5, + "type": "dht", + "zone": "11", + }, + ], + "switches": [ + { + "activation": "high", + "momentary": None, + "name": "Konnected 445566 Actuator 4", + "pause": None, + "repeat": None, + "state": None, + "zone": "4", + }, + { + "activation": "low", + "momentary": 50, + "name": "switcher", + "pause": 100, + "repeat": 4, + "state": None, + "zone": "8", + }, + { + "activation": "high", + "momentary": None, + "name": "Konnected 445566 Actuator out1", + "pause": None, + "repeat": None, + "state": None, + "zone": "out1", + }, + { + "activation": "high", + "momentary": None, + "name": "Konnected 445566 Actuator alarm1", + "pause": None, + "repeat": None, + "state": None, + "zone": "alarm1", + }, + ], + } + } From 15ed086ed2a1b01ce362cd458c77e22f0fe05f6a Mon Sep 17 00:00:00 2001 From: Massimiliano Cannarozzo Date: Tue, 11 Feb 2020 22:10:33 +0100 Subject: [PATCH 199/378] Fix set volume level (#31731) afsapi requires an `int` in the 0-20 range but we receive a `float` in the 0.0-1.0 range so we have to cast and offset it --- homeassistant/components/frontier_silicon/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 010420d0f98..627c3c079b9 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -275,7 +275,7 @@ class AFSAPIDevice(MediaPlayerDevice): async def async_set_volume_level(self, volume): """Set volume command.""" - await self.fs_device.set_volume(volume) + await self.fs_device.set_volume(int(volume * 20)) async def async_select_source(self, source): """Select input source.""" From 378bd8438bac13aa87beab85e041e54c5bce93ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Tue, 11 Feb 2020 22:11:02 +0100 Subject: [PATCH 200/378] Update Modbus service manifest (#31727) --- homeassistant/components/modbus/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 2158528814f..8c11209570b 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -4,9 +4,11 @@ write_coil: address: {description: Address of the register to write to., example: 0} state: {description: State to write., example: false} unit: {description: Address of the modbus unit., example: 21} + hub: {description: Optional Modbus hub name. A hub with the name 'default' is used if not specified., example: "hub1"} write_register: description: Write to a modbus holding register. fields: address: {description: Address of the holding register to write to., example: 0} unit: {description: Address of the modbus unit., example: 21} value: {description: Value (single value or array) to write., example: "0 or [4,0]"} + hub: {description: Optional Modbus hub name. A hub with the name 'default' is used if not specified., example: "hub1"} From 9e233070a08dcfc44e25ae9b4fee101390146457 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 12 Feb 2020 00:31:49 +0000 Subject: [PATCH 201/378] [ci skip] Translation update --- .../components/deconz/.translations/fr.json | 1 + .../konnected/.translations/da.json | 8 +++++++ .../konnected/.translations/en.json | 1 - .../components/melcloud/.translations/ca.json | 23 +++++++++++++++++++ .../components/melcloud/.translations/ru.json | 23 +++++++++++++++++++ .../components/vizio/.translations/fr.json | 3 ++- 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/konnected/.translations/da.json create mode 100644 homeassistant/components/melcloud/.translations/ca.json create mode 100644 homeassistant/components/melcloud/.translations/ru.json diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index c900bdab6ab..214c887cc34 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -77,6 +77,7 @@ "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", + "remote_double_tap_any_side": "Appareil double tap\u00e9 de n\u2019importe quel c\u00f4t\u00e9", "remote_falling": "Appareil en chute libre", "remote_flip_180_degrees": "Dispositif retourn\u00e9 \u00e0 180 degr\u00e9s", "remote_flip_90_degrees": "Dispositif retourn\u00e9 \u00e0 90 degr\u00e9s", diff --git a/homeassistant/components/konnected/.translations/da.json b/homeassistant/components/konnected/.translations/da.json new file mode 100644 index 00000000000..b8b8acc2f49 --- /dev/null +++ b/homeassistant/components/konnected/.translations/da.json @@ -0,0 +1,8 @@ +{ + "options": { + "error": { + "one": "EN", + "other": "ANDEN" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json index 6459fcebc53..cb6d2d24ff1 100644 --- a/homeassistant/components/konnected/.translations/en.json +++ b/homeassistant/components/konnected/.translations/en.json @@ -29,7 +29,6 @@ "abort": { "not_konn_panel": "Not a recognized Konnected.io device" }, - "error": {}, "step": { "options_binary": { "data": { diff --git a/homeassistant/components/melcloud/.translations/ca.json b/homeassistant/components/melcloud/.translations/ca.json new file mode 100644 index 00000000000..1dc5156f7e7 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3 MELCloud ja est\u00e0 configurada amb aquest correu electr\u00f2nic. El testimoni d'acc\u00e9s s'ha actualitzat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya de MELCloud.", + "username": "Correu electr\u00f2nic d'inici de sessi\u00f3 a MELCloud." + }, + "description": "Connecta\u2019t amb el teu compte de MELCloud.", + "title": "Connexi\u00f3 amb MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/ru.json b/homeassistant/components/melcloud/.translations/ru.json new file mode 100644 index 00000000000..d4bab0e417e --- /dev/null +++ b/homeassistant/components/melcloud/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MELCloud \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c MELCloud.", + "username": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u044f \u043f\u043e\u0447\u0442\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 MELCloud." + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u0441\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c MELCloud.", + "title": "MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index a6ae77365f4..cf0cdea787f 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -33,7 +33,8 @@ "step": { "init": { "data": { - "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)" + "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)", + "volume_step": "Taille du pas de volume" }, "title": "Mettre \u00e0 jour les options de Vizo SmartCast" } From 787edf94173e9f3f9f7f704612e35a01f0a3609b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 12 Feb 2020 01:37:48 +0000 Subject: [PATCH 202/378] pyipma version bump (#31739) --- homeassistant/components/ipma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index cd66ce7461b..02d4e459f72 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Português do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==2.0.2"], + "requirements": ["pyipma==2.0.3"], "dependencies": [], "codeowners": ["@dgomes", "@abmantis"] } diff --git a/requirements_all.txt b/requirements_all.txt index 70d87011a71..a80435d71ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ pyicloud==0.9.2 pyintesishome==1.6 # homeassistant.components.ipma -pyipma==2.0.2 +pyipma==2.0.3 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81c3021e2e9..063c5c72f47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -473,7 +473,7 @@ pyhomematic==0.1.64 pyicloud==0.9.2 # homeassistant.components.ipma -pyipma==2.0.2 +pyipma==2.0.3 # homeassistant.components.iqvia pyiqvia==0.2.1 From 54eb740ff6572f69337b29998e49585e398b2a45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Feb 2020 19:43:56 -0600 Subject: [PATCH 203/378] Read door open/close events from the activity log. (#31732) As polling for lock status is every 15 minutes, we read lock and unlock events from the activity log. An upstream update of py-august was needed to expose the door open and close events. Door open and close events are now seen within a few seconds instead of delayed 15+ minutes. --- homeassistant/components/august/__init__.py | 32 +++++++++++-- .../components/august/binary_sensor.py | 47 +++++++++++++++++++ homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index a646ee2bad5..c6a0f90fcaa 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -205,6 +205,7 @@ class AugustData: self._house_ids.add(device.house_id) self._doorbell_detail_by_id = {} + self._door_last_state_update_time_utc_by_id = {} self._lock_last_status_update_time_utc_by_id = {} self._lock_status_by_id = {} self._lock_detail_by_id = {} @@ -290,6 +291,16 @@ class AugustData: _LOGGER.debug("Completed retrieving doorbell details") self._doorbell_detail_by_id = detail_by_id + def update_door_state(self, lock_id, door_state, update_start_time_utc): + """Set the door status and last status update time. + + This is called when newer activity is detected on the activity feed + in order to keep the internal data in sync + """ + self._door_state_by_id[lock_id] = door_state + self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc + return True + def update_lock_status(self, lock_id, lock_status, update_start_time_utc): """Set the lock status and last status update time. @@ -330,7 +341,8 @@ class AugustData: def _update_locks_status(self): status_by_id = {} state_by_id = {} - last_status_update_by_id = {} + lock_last_status_update_by_id = {} + door_last_state_update_by_id = {} _LOGGER.debug("Start retrieving lock and door status") for lock in self._locks: @@ -346,9 +358,10 @@ class AugustData: # Since there is a a race condition between calling the # lock and activity apis, we set the last update time # BEFORE making the api call since we will compare this - # to activity later we want activity to win over stale lock + # to activity later we want activity to win over stale lock/door # state. - last_status_update_by_id[lock.device_id] = update_start_time_utc + lock_last_status_update_by_id[lock.device_id] = update_start_time_utc + door_last_state_update_by_id[lock.device_id] = update_start_time_utc except RequestException as ex: _LOGGER.error( "Request error trying to retrieve lock and door status for %s. %s", @@ -365,7 +378,8 @@ class AugustData: _LOGGER.debug("Completed retrieving lock and door status") self._lock_status_by_id = status_by_id self._door_state_by_id = state_by_id - self._lock_last_status_update_time_utc_by_id = last_status_update_by_id + self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id + self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id def get_last_lock_status_update_time_utc(self, lock_id): """Return the last time that a lock status update was seen from the august API.""" @@ -377,6 +391,16 @@ class AugustData: return self._lock_last_status_update_time_utc_by_id[lock_id] + def get_last_door_state_update_time_utc(self, lock_id): + """Return the last time that a door status update was seen from the august API.""" + # Since the activity api is called more frequently than + # the lock api it is possible that the door has not + # been updated yet + if lock_id not in self._door_last_state_update_time_utc_by_id: + return dt.utc_from_timestamp(0) + + return self._door_last_state_update_time_utc_by_id[lock_id] + @Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES) def _update_locks_detail(self): detail_by_id = {} diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index f840d3db532..0984eb0629f 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -6,6 +6,7 @@ from august.activity import ActivityType from august.lock import LockDoorStatus from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.util import dt from . import DATA_AUGUST @@ -137,6 +138,52 @@ class AugustDoorBinarySensor(BinarySensorDevice): self._state = self._state == LockDoorStatus.OPEN + activity = self._data.get_latest_device_activity( + self._door.device_id, ActivityType.DOOR_OPERATION + ) + + if activity is not None: + self._sync_door_activity(activity) + + def _update_door_state(self, door_state, update_start_time): + new_state = door_state == LockDoorStatus.OPEN + if self._state != new_state: + self._state = new_state + self._data.update_door_state( + self._door.device_id, door_state, update_start_time + ) + + def _sync_door_activity(self, activity): + """Check the activity for the latest door open/close activity (events). + + We use this to determine the door state in between calls to the lock + api as we update it more frequently + """ + last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc( + self._door.device_id + ) + activity_end_time_utc = dt.as_utc(activity.activity_end_time) + + if activity_end_time_utc > last_door_state_update_time_utc: + _LOGGER.debug( + "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]", + self.name, + activity.action, + activity_end_time_utc, + last_door_state_update_time_utc, + ) + activity_start_time_utc = dt.as_utc(activity.activity_start_time) + if activity.action == "doorclosed": + self._update_door_state(LockDoorStatus.CLOSED, activity_start_time_utc) + elif activity.action == "dooropen": + self._update_door_state(LockDoorStatus.OPEN, activity_start_time_utc) + else: + _LOGGER.info( + "Unhandled door activity action %s for %s", + activity.action, + self.name, + ) + @property def unique_id(self) -> str: """Get the unique of the door open binary sensor.""" diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index bacd7346ca7..f74e20f38c2 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.8.1"], + "requirements": ["py-august==0.11.0"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index a80435d71ff..0dac9a0b481 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1074,7 +1074,7 @@ pushover_complete==1.1.1 pwmled==1.4.1 # homeassistant.components.august -py-august==0.8.1 +py-august==0.11.0 # homeassistant.components.canary py-canary==0.5.0 From d1b0ab736d31468795649662f28cb986866bcbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 12 Feb 2020 03:54:19 +0200 Subject: [PATCH 204/378] Improve Huawei LTE timeouting/stalling request behavior (#31710) * Suppress data get timeout exceptions for 30s after notify attempts At least SMS send failures may put the API in a state where it times out some (but not all) data get operations for some time, e.g. 25s. Closes https://github.com/home-assistant/home-assistant/issues/30827 * Do not pile up duplicate data requests Do not add another request for a piece of data for which a previous request is still in progress. For example failing SMS sends are known to stall some (but not all) requests for some time, and firing up more is not going to help. --- .../components/huawei_lte/__init__.py | 22 +++++++++++++++++++ homeassistant/components/huawei_lte/const.py | 1 + homeassistant/components/huawei_lte/notify.py | 3 +++ 3 files changed, 26 insertions(+) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 1d54f972907..d3b2d5b1abd 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta from functools import partial import ipaddress import logging +import time from typing import Any, Callable, Dict, List, Set, Tuple from urllib.parse import urlparse @@ -65,6 +66,7 @@ from .const import ( KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + NOTIFY_SUPPRESS_TIMEOUT, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, SERVICE_RESUME_INTEGRATION, @@ -138,9 +140,11 @@ class Router: init=False, factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), ) + inflight_gets: Set[str] = attr.ib(init=False, factory=set) unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client suspended = attr.ib(init=False, default=False) + notify_last_attempt: float = attr.ib(init=False, default=-1) def __attrs_post_init__(self): """Set up internal state on init.""" @@ -167,6 +171,10 @@ class Router: def _get_data(self, key: str, func: Callable[[None], Any]) -> None: if not self.subscriptions.get(key): return + if key in self.inflight_gets: + _LOGGER.debug("Skipping already inflight get for %s", key) + return + self.inflight_gets.add(key) _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) try: self.data[key] = func() @@ -189,7 +197,21 @@ class Router: "%s requires authorization, excluding from future updates", key ) self.subscriptions.pop(key) + except Timeout: + grace_left = ( + self.notify_last_attempt - time.monotonic() + NOTIFY_SUPPRESS_TIMEOUT + ) + if grace_left > 0: + _LOGGER.debug( + "%s timed out, %.1fs notify timeout suppress grace remaining", + key, + grace_left, + exc_info=True, + ) + else: + raise finally: + self.inflight_gets.discard(key) _LOGGER.debug("%s=%s", key, self.data.get(key)) def update(self) -> None: diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index c6837fce06c..e227f06cf28 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -12,6 +12,7 @@ UNIT_BYTES = "B" UNIT_SECONDS = "s" CONNECTION_TIMEOUT = 10 +NOTIFY_SUPPRESS_TIMEOUT = 30 SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics" SERVICE_REBOOT = "reboot" diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 5619a5d702c..91cc8864eb0 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,6 +1,7 @@ """Support for Huawei LTE router notifications.""" import logging +import time from typing import Any, List import attr @@ -57,3 +58,5 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): _LOGGER.debug("Sent to %s: %s", targets, resp) except ResponseErrorException as ex: _LOGGER.error("Could not send to %s: %s", targets, ex) + finally: + self.router.notify_last_attempt = time.monotonic() From 891f13eefe3399cbb41c8a7aba18efa6c6a0dac0 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 12 Feb 2020 03:02:13 +0100 Subject: [PATCH 205/378] Fix missing device class in netatmo binary sensors (#31693) * Bring back device class * Add door tag sensors types * Actually discover individual tags per camera --- .../components/netatmo/binary_sensor.py | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 6d0de6dcceb..5f419bda2c2 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -24,7 +24,10 @@ PRESENCE_SENSOR_TYPES = { } TAG_SENSOR_TYPES = {"Tag Vibration": "vibration", "Tag Open": "opening"} -SENSOR_TYPES = {"NACamera": WELCOME_SENSOR_TYPES, "NOC": PRESENCE_SENSOR_TYPES} +SENSOR_TYPES = { + "NACamera": WELCOME_SENSOR_TYPES, + "NOC": PRESENCE_SENSOR_TYPES, +} CONF_HOME = "home" CONF_CAMERAS = "cameras" @@ -61,12 +64,28 @@ async def async_setup_entry(hass, entry, async_add_entities): sensor_types.update(SENSOR_TYPES[camera["type"]]) # Tags are only supported with Netatmo Welcome indoor cameras - if camera["type"] == "NACamera" and data.get_modules(camera["id"]): - sensor_types.update(TAG_SENSOR_TYPES) + modules = data.get_modules(camera["id"]) + if camera["type"] == "NACamera" and modules: + for module in modules: + for sensor_type in TAG_SENSOR_TYPES: + _LOGGER.debug( + "Adding camera tag %s (%s)", + module["name"], + module["id"], + ) + entities.append( + NetatmoBinarySensor( + data, + camera["id"], + home_id, + sensor_type, + module["id"], + ) + ) - for sensor_name in sensor_types: + for sensor_type in sensor_types: entities.append( - NetatmoBinarySensor(data, camera["id"], home_id, sensor_name) + NetatmoBinarySensor(data, camera["id"], home_id, sensor_type) ) except pyatmo.NoDevice: _LOGGER.debug("No camera entities to add") @@ -115,6 +134,15 @@ class NetatmoBinarySensor(BinarySensorDevice): """Return the unique ID for this sensor.""" return self._unique_id + @property + def device_class(self): + """Return the class of this sensor.""" + if self._camera_type == "NACamera": + return WELCOME_SENSOR_TYPES.get(self._sensor_type) + if self._camera_type == "NOC": + return PRESENCE_SENSOR_TYPES.get(self._sensor_type) + return TAG_SENSOR_TYPES.get(self._sensor_type) + @property def device_info(self): """Return the device info for the sensor.""" From f5be9ef7fb43a5f14e34d2435c64499d7e48c82c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Feb 2020 00:13:54 -0600 Subject: [PATCH 206/378] Refresh the august access token when needed (#31735) * Refresh the august access token when needed. Currently august will stop working when the token expires about every six month. This resolves issue #23788 * Make refresh_access_token_if_needed private since we do not want additional callers * Add init --- CODEOWNERS | 1 + homeassistant/components/august/__init__.py | 35 +++++++++++++-- homeassistant/components/august/manifest.json | 2 +- requirements_test_all.txt | 3 ++ tests/components/august/__init__.py | 1 + tests/components/august/test_init.py | 44 +++++++++++++++++++ 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tests/components/august/__init__.py create mode 100644 tests/components/august/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 658fca1e8fc..48a23bba619 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,7 @@ homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs +homeassistant/components/august/* @bdraco homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index c6a0f90fcaa..c7b81646b42 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -136,7 +136,7 @@ def setup_august(hass, config, api, authenticator): if DOMAIN in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) - hass.data[DATA_AUGUST] = AugustData(hass, api, authentication.access_token) + hass.data[DATA_AUGUST] = AugustData(hass, api, authentication, authenticator) for component in AUGUST_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -193,11 +193,14 @@ def setup(hass, config): class AugustData: """August data object.""" - def __init__(self, hass, api, access_token): + def __init__(self, hass, api, authentication, authenticator): """Init August data object.""" self._hass = hass self._api = api - self._access_token = access_token + self._authenticator = authenticator + self._access_token = authentication.access_token + self._access_token_expires = authentication.access_token_expires + self._doorbells = self._api.get_doorbells(self._access_token) or [] self._locks = self._api.get_operable_locks(self._access_token) or [] self._house_ids = set() @@ -227,6 +230,21 @@ class AugustData: """Return a list of locks.""" return self._locks + def _refresh_access_token_if_needed(self): + """Refresh the august access token if needed.""" + + if self._authenticator.should_refresh(): + refreshed_authentication = self._authenticator.refresh_access_token( + force=False + ) + _LOGGER.info( + "Refreshed august access token. The old token expired at %s, and the new token expires at %s", + self._access_token_expires, + refreshed_authentication.access_token_expires, + ) + self._access_token = refreshed_authentication.access_token + self._access_token_expires = refreshed_authentication.access_token_expires + def get_device_activities(self, device_id, *activity_types): """Return a list of activities.""" _LOGGER.debug("Getting device activities") @@ -245,6 +263,17 @@ class AugustData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): """Update data object with latest from August API.""" + + # This is the only place we refresh the api token + # in order to avoid multiple threads from doing it at the same time + # since there will only be one activity refresh at a time + # + # In the future when this module is converted to async we should + # use a lock to prevent all api calls while the token + # is being refreshed as this is a better solution + # + self._refresh_access_token_if_needed() + _LOGGER.debug("Start retrieving device activities") for house_id in self.house_ids: _LOGGER.debug("Updating device activity for house id %s", house_id) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f74e20f38c2..fb5bb3ef3ef 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "requirements": ["py-august==0.11.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": ["@bdraco"] } diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 063c5c72f47..da710af4722 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,6 +389,9 @@ pure-python-adb==0.2.2.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 +# homeassistant.components.august +py-august==0.11.0 + # homeassistant.components.canary py-canary==0.5.0 diff --git a/tests/components/august/__init__.py b/tests/components/august/__init__.py new file mode 100644 index 00000000000..156b6170511 --- /dev/null +++ b/tests/components/august/__init__.py @@ -0,0 +1 @@ +"""Tests for the august component.""" diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py new file mode 100644 index 00000000000..e84df35b6b1 --- /dev/null +++ b/tests/components/august/test_init.py @@ -0,0 +1,44 @@ +"""The tests for the august platform.""" +from unittest.mock import MagicMock, PropertyMock + +from homeassistant.components import august + + +def _mock_august_authenticator(): + authenticator = MagicMock(name="august.authenticator") + authenticator.should_refresh = MagicMock( + name="august.authenticator.should_refresh", return_value=0 + ) + authenticator.refresh_access_token = MagicMock( + name="august.authenticator.refresh_access_token" + ) + return authenticator + + +def _mock_august_authentication(token_text, token_timestamp): + authentication = MagicMock(name="august.authentication") + type(authentication).access_token = PropertyMock(return_value=token_text) + type(authentication).access_token_expires = PropertyMock( + return_value=token_timestamp + ) + return authentication + + +def test__refresh_access_token(): + """Set up things to be run when tests are started.""" + authentication = _mock_august_authentication("original_token", 1234) + authenticator = _mock_august_authenticator() + data = august.AugustData( + MagicMock(name="hass"), MagicMock(name="api"), authentication, authenticator + ) + data._refresh_access_token_if_needed() + authenticator.refresh_access_token.assert_not_called() + + authenticator.should_refresh.return_value = 1 + authenticator.refresh_access_token.return_value = _mock_august_authentication( + "new_token", 5678 + ) + data._refresh_access_token_if_needed() + authenticator.refresh_access_token.assert_called() + assert data._access_token == "new_token" + assert data._access_token_expires == 5678 From 0700d38d1f875baf37417c6e892da6a236b695df Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 11 Feb 2020 23:56:22 -0800 Subject: [PATCH 207/378] =?UTF-8?q?Add=20new=20webhook=20action=20to=20all?= =?UTF-8?q?ow=20enabling=20encryption=20in=20an=20exis=E2=80=A6=20(#31743)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new webhook action to allow enabling encryption in an existing registration * Harden tests * Make requested fixes --- homeassistant/components/mobile_app/const.py | 2 + .../components/mobile_app/webhook.py | 35 ++++- tests/components/mobile_app/test_webhook.py | 125 ++++++++++++++---- 3 files changed, 137 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 720cf7106e7..f43f1c88396 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -52,6 +52,8 @@ ATTR_WEBHOOK_ENCRYPTED = "encrypted" ATTR_WEBHOOK_ENCRYPTED_DATA = "encrypted_data" ATTR_WEBHOOK_TYPE = "type" +ERR_ENCRYPTION_ALREADY_ENABLED = "encryption_already_enabled" +ERR_ENCRYPTION_NOT_AVAILABLE = "encryption_not_available" ERR_ENCRYPTION_REQUIRED = "encryption_required" ERR_SENSOR_NOT_REGISTERED = "not_registered" ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id" diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 3a477d89925..c47f38986a1 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,8 +1,10 @@ """Webhook handlers for mobile_app.""" from functools import wraps import logging +import secrets -from aiohttp.web import HTTPBadRequest, Request, Response +from aiohttp.web import HTTPBadRequest, Request, Response, json_response +from nacl.secret import SecretBox import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -71,6 +73,8 @@ from .const import ( DATA_DELETED_IDS, DATA_STORE, DOMAIN, + ERR_ENCRYPTION_ALREADY_ENABLED, + ERR_ENCRYPTION_NOT_AVAILABLE, ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, @@ -84,6 +88,7 @@ from .helpers import ( registration_context, safe_registration, savable_state, + supports_encryption, webhook_response, ) @@ -307,6 +312,34 @@ async def webhook_update_registration(hass, config_entry, data): ) +@WEBHOOK_COMMANDS.register("enable_encryption") +async def webhook_enable_encryption(hass, config_entry, data): + """Handle a encryption enable webhook.""" + if config_entry.data[ATTR_SUPPORTS_ENCRYPTION]: + _LOGGER.warning( + "Refusing to enable encryption for %s because it is already enabled!", + config_entry.data[ATTR_DEVICE_NAME], + ) + return error_response( + ERR_ENCRYPTION_ALREADY_ENABLED, "Encryption already enabled" + ) + + if not supports_encryption(): + _LOGGER.warning( + "Unable to enable encryption for %s because libsodium is unavailable!", + config_entry.data[ATTR_DEVICE_NAME], + ) + return error_response(ERR_ENCRYPTION_NOT_AVAILABLE, "Encryption is unavailable") + + secret = secrets.token_hex(SecretBox.KEY_SIZE) + + data = {**config_entry.data, ATTR_SUPPORTS_ENCRYPTION: True, CONF_SECRET: secret} + + hass.config_entries.async_update_entry(config_entry, data=data) + + return json_response({"secret": secret}) + + @WEBHOOK_COMMANDS.register("register_sensor") @validate_schema( { diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 3df71c34781..39837543a47 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,5 +1,4 @@ """Webhook tests for mobile_app.""" - import logging import pytest @@ -17,6 +16,53 @@ from tests.common import async_mock_service _LOGGER = logging.getLogger(__name__) +def encrypt_payload(secret_key, payload): + """Return a encrypted payload given a key and dictionary of data.""" + try: + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + keylen = SecretBox.KEY_SIZE + prepped_key = secret_key.encode("utf-8") + prepped_key = prepped_key[:keylen] + prepped_key = prepped_key.ljust(keylen, b"\0") + + payload = json.dumps(payload).encode("utf-8") + + return ( + SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + ) + + +def decrypt_payload(secret_key, encrypted_data): + """Return a decrypted payload given a key and a string of encrypted data.""" + try: + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + keylen = SecretBox.KEY_SIZE + prepped_key = secret_key.encode("utf-8") + prepped_key = prepped_key[:keylen] + prepped_key = prepped_key.ljust(keylen, b"\0") + + decrypted_data = SecretBox(prepped_key).decrypt( + encrypted_data, encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + return json.loads(decrypted_data) + + async def test_webhook_handle_render_template(create_registrations, webhook_client): """Test that we render templates properly.""" resp = await webhook_client.post( @@ -166,23 +212,8 @@ async def test_webhook_returns_error_incorrect_json( async def test_webhook_handle_decryption(webhook_client, create_registrations): """Test that we can encrypt/decrypt properly.""" - try: - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - return - - import json - - keylen = SecretBox.KEY_SIZE - key = create_registrations[0]["secret"].encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b"\0") - - payload = json.dumps(RENDER_TEMPLATE["data"]).encode("utf-8") - - data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + key = create_registrations[0]["secret"] + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} @@ -195,12 +226,9 @@ async def test_webhook_handle_decryption(webhook_client, create_registrations): webhook_json = await resp.json() assert "encrypted_data" in webhook_json - decrypted_data = SecretBox(key).decrypt( - webhook_json["encrypted_data"], encoder=Base64Encoder - ) - decrypted_data = decrypted_data.decode("utf-8") + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) - assert json.loads(decrypted_data) == {"one": "Hello world"} + assert decrypted_data == {"one": "Hello world"} async def test_webhook_requires_encryption(webhook_client, create_registrations): @@ -219,7 +247,7 @@ async def test_webhook_requires_encryption(webhook_client, create_registrations) async def test_webhook_update_location(hass, webhook_client, create_registrations): - """Test that encrypted registrations only accept encrypted data.""" + """Test that location can be updated.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json={ @@ -236,3 +264,52 @@ async def test_webhook_update_location(hass, webhook_client, create_registration assert state.attributes["longitude"] == 2.0 assert state.attributes["gps_accuracy"] == 10 assert state.attributes["altitude"] == -10 + + +async def test_webhook_enable_encryption(hass, webhook_client, create_registrations): + """Test that encryption can be added to a reg initially created without.""" + webhook_id = create_registrations[1]["webhook_id"] + + enable_enc_resp = await webhook_client.post( + "/api/webhook/{}".format(webhook_id), json={"type": "enable_encryption"}, + ) + + assert enable_enc_resp.status == 200 + + enable_enc_json = await enable_enc_resp.json() + assert len(enable_enc_json) == 1 + assert CONF_SECRET in enable_enc_json + + key = enable_enc_json["secret"] + + enc_required_resp = await webhook_client.post( + "/api/webhook/{}".format(webhook_id), json=RENDER_TEMPLATE, + ) + + assert enc_required_resp.status == 400 + + enc_required_json = await enc_required_resp.json() + assert "error" in enc_required_json + assert enc_required_json["success"] is False + assert enc_required_json["error"]["code"] == "encryption_required" + + enc_data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + + container = { + "type": "render_template", + "encrypted": True, + "encrypted_data": enc_data, + } + + enc_resp = await webhook_client.post( + "/api/webhook/{}".format(webhook_id), json=container + ) + + assert enc_resp.status == 200 + + enc_json = await enc_resp.json() + assert "encrypted_data" in enc_json + + decrypted_data = decrypt_payload(key, enc_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} From cde414e1dfbf21d9b2ee7f445386ccdc2ebce419 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Feb 2020 03:59:59 -0800 Subject: [PATCH 208/378] Use set for dependency lookup in hassfest (#31746) --- script/hassfest/dependencies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 52c8bfecf95..a600eea141d 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -110,7 +110,7 @@ ALLOWED_USED_COMPONENTS = { "stream", # Stream cannot install on all systems, can be imported without reqs. } -IGNORE_VIOLATIONS = [ +IGNORE_VIOLATIONS = { # Has same requirement, gets defaults. ("sql", "recorder"), # Sharing a base class @@ -136,7 +136,7 @@ IGNORE_VIOLATIONS = [ "dwd_weather_warnings", # Should be rewritten to use own data fetcher "scrape", -] +} def calc_allowed_references(integration: Integration) -> Set[str]: From 06f06427b65c52f2cd2c9ec7e2ca2fe956c900f9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Feb 2020 16:29:06 +0100 Subject: [PATCH 209/378] Fix spelling of ecobee in manifest (#31751) --- homeassistant/components/ecobee/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 32b58964926..5df89c3d90d 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -1,6 +1,6 @@ { "domain": "ecobee", - "name": "Ecobee", + "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", "dependencies": [], From 378c432f6da4d30917eeda891045c437da1fc11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Wed, 12 Feb 2020 18:37:16 +0100 Subject: [PATCH 210/378] Add availability status to Modbus entities and improve error handling (#31073) --- .../components/modbus/binary_sensor.py | 47 ++++-- homeassistant/components/modbus/climate.py | 58 +++++-- homeassistant/components/modbus/sensor.py | 62 ++++--- homeassistant/components/modbus/switch.py | 151 ++++++++++++++---- 4 files changed, 239 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 6959f3b47b8..02638c2a786 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -2,6 +2,8 @@ import logging from typing import Optional +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -83,6 +85,7 @@ class ModbusBinarySensor(BinarySensorDevice): self._device_class = device_class self._input_type = input_type self._value = None + self._available = True @property def name(self): @@ -99,18 +102,38 @@ class ModbusBinarySensor(BinarySensorDevice): """Return the device class of the sensor.""" return self._device_class + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + def update(self): """Update the state of the sensor.""" - if self._input_type == INPUT_TYPE_COIL: - result = self._hub.read_coils(self._slave, self._address, 1) - else: - result = self._hub.read_discrete_inputs(self._slave, self._address, 1) try: - self._value = result.bits[0] - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, address %s", - self._hub.name, - self._slave, - self._address, - ) + if self._input_type == INPUT_TYPE_COIL: + result = self._hub.read_coils(self._slave, self._address, 1) + else: + result = self._hub.read_discrete_inputs(self._slave, self._address, 1) + except ConnectionException: + self._set_unavailable() + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + self._value = result.bits[0] + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, address %s", + self._hub.name, + self._slave, + self._address, + ) + self._available = False diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 99ea686543d..29b6eb1a9fb 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -1,7 +1,10 @@ """Support for Generic Modbus Thermostats.""" import logging import struct +from typing import Optional +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -140,6 +143,7 @@ class ModbusThermostat(ClimateDevice): self._min_temp = min_temp self._temp_step = temp_step self._structure = ">f" + self._available = True data_types = { DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}, @@ -156,8 +160,10 @@ class ModbusThermostat(ClimateDevice): def update(self): """Update Target & Current Temperature.""" - self._target_temperature = self.read_register(self._target_temperature_register) - self._current_temperature = self.read_register( + self._target_temperature = self._read_register( + self._target_temperature_register + ) + self._current_temperature = self._read_register( self._current_temperature_register ) @@ -215,20 +221,27 @@ class ModbusThermostat(ClimateDevice): return byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] + self._write_register(self._target_temperature_register, register_value) - try: - self.write_register(self._target_temperature_register, register_value) - except AttributeError as ex: - _LOGGER.error(ex) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available - def read_register(self, register): + def _read_register(self, register) -> Optional[float]: """Read holding register using the Modbus hub slave.""" try: result = self._hub.read_holding_registers( self._slave, register, self._count ) - except AttributeError as ex: - _LOGGER.error(ex) + except ConnectionException: + self._set_unavailable(register) + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable(register) + return + byte_string = b"".join( [x.to_bytes(2, byteorder="big") for x in result.registers] ) @@ -237,8 +250,29 @@ class ModbusThermostat(ClimateDevice): (self._scale * val) + self._offset, f".{self._precision}f" ) register_value = float(register_value) + self._available = True + return register_value - def write_register(self, register, value): - """Write register using the Modbus hub slave.""" - self._hub.write_registers(self._slave, register, [value, 0]) + def _write_register(self, register, value): + """Write holding register using the Modbus hub slave.""" + try: + self._hub.write_registers(self._slave, register, [value, 0]) + except ConnectionException: + self._set_unavailable(register) + return + + self._available = True + + def _set_unavailable(self, register): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, register %s", + self._hub.name, + self._slave, + register, + ) + self._available = False diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 484382983ac..3ffbe6d8c40 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,6 +3,8 @@ import logging import struct from typing import Any, Optional, Union +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA @@ -184,6 +186,7 @@ class ModbusRegisterSensor(RestoreEntity): self._structure = structure self._device_class = device_class self._value = None + self._available = True async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -212,30 +215,34 @@ class ModbusRegisterSensor(RestoreEntity): """Return the device class of the sensor.""" return self._device_class + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + def update(self): """Update the state of the sensor.""" - if self._register_type == REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers( - self._slave, self._register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._register, self._count - ) - val = 0 - try: - registers = result.registers - if self._reverse_order: - registers.reverse() - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, register %s", - self._hub.name, - self._slave, - self._register, - ) + if self._register_type == REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers( + self._slave, self._register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, self._count + ) + except ConnectionException: + self._set_unavailable() return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + registers = result.registers + if self._reverse_order: + registers.reverse() + byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) val = struct.unpack(self._structure, byte_string)[0] val = self._scale * val + self._offset @@ -245,3 +252,18 @@ class ModbusRegisterSensor(RestoreEntity): self._value += "." + "0" * self._precision else: self._value = f"{val:.{self._precision}f}" + + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, address %s", + self._hub.name, + self._slave, + self._register, + ) + self._available = False diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 0ed33dedb57..8c1e3b53834 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,6 +1,9 @@ """Support for Modbus switches.""" import logging +from typing import Optional +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA @@ -116,6 +119,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): self._slave = int(slave) if slave else None self._coil = int(coil) self._is_on = None + self._available = True async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -134,26 +138,62 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Return the name of the switch.""" return self._name + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + def turn_on(self, **kwargs): """Set switch on.""" - self._hub.write_coil(self._slave, self._coil, True) + self._write_coil(self._coil, True) def turn_off(self, **kwargs): """Set switch off.""" - self._hub.write_coil(self._slave, self._coil, False) + self._write_coil(self._coil, False) def update(self): """Update the state of the switch.""" - result = self._hub.read_coils(self._slave, self._coil, 1) + self._is_on = self._read_coil(self._coil) + + def _read_coil(self, coil) -> Optional[bool]: + """Read coil using the Modbus hub slave.""" try: - self._is_on = bool(result.bits[0]) - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, coil %s", - self._hub.name, - self._slave, - self._coil, - ) + result = self._hub.read_coils(self._slave, coil, 1) + except ConnectionException: + self._set_unavailable() + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + value = bool(result.bits[0]) + self._available = True + + return value + + def _write_coil(self, coil, value): + """Write coil using the Modbus hub slave.""" + try: + self._hub.write_coil(self._slave, coil, value) + except ConnectionException: + self._set_unavailable() + return + + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, coil %s", + self._hub.name, + self._slave, + self._coil, + ) + self._available = False class ModbusRegisterSwitch(ModbusCoilSwitch): @@ -184,6 +224,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._verify_state = verify_state self._verify_register = verify_register if verify_register else self._register self._register_type = register_type + self._available = True if state_on is not None: self._state_on = state_on @@ -199,46 +240,86 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): def turn_on(self, **kwargs): """Set switch on.""" - self._hub.write_register(self._slave, self._register, self._command_on) - if not self._verify_state: - self._is_on = True + + # Only holding register is writable + if self._register_type == REGISTER_TYPE_HOLDING: + self._write_register(self._command_on) + if not self._verify_state: + self._is_on = True def turn_off(self, **kwargs): """Set switch off.""" - self._hub.write_register(self._slave, self._register, self._command_off) - if not self._verify_state: - self._is_on = False + + # Only holding register is writable + if self._register_type == REGISTER_TYPE_HOLDING: + self._write_register(self._command_off) + if not self._verify_state: + self._is_on = False + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available def update(self): """Update the state of the switch.""" if not self._verify_state: return - value = 0 - if self._register_type == REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers(self._slave, self._register, 1) - else: - result = self._hub.read_holding_registers(self._slave, self._register, 1) - - try: - value = int(result.registers[0]) - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, register %s", - self._hub.name, - self._slave, - self._verify_register, - ) - + value = self._read_register() if value == self._state_on: self._is_on = True elif value == self._state_off: self._is_on = False - else: + elif value is not None: _LOGGER.error( "Unexpected response from hub %s, slave %s register %s, got 0x%2x", self._hub.name, self._slave, - self._verify_register, + self._register, value, ) + + def _read_register(self) -> Optional[int]: + try: + if self._register_type == REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers(self._slave, self._register, 1) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, 1 + ) + except ConnectionException: + self._set_unavailable() + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + value = int(result.registers[0]) + self._available = True + + return value + + def _write_register(self, value): + """Write holding register using the Modbus hub slave.""" + try: + self._hub.write_register(self._slave, self._register, value) + except ConnectionException: + self._set_unavailable() + return + + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, register %s", + self._hub.name, + self._slave, + self._register, + ) + self._available = False From 8498ca37cd3093b3c9230f007797fedee1e5f8d0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 12 Feb 2020 18:53:07 +0100 Subject: [PATCH 211/378] Fix moving average test for discrete derivative sensor (#31750) * fix test_data_moving_average_for_discrete_sensor After https://github.com/home-assistant/home-assistant/pull/31717 the test didn't actually test anything anymore. This fixes that. * make the test faster --- tests/components/derivative/test_sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 9ffa29571b7..9a26d2de5ce 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -124,18 +124,18 @@ async def test_dataSet6(hass): async def test_data_moving_average_for_discrete_sensor(hass): """Test derivative sensor state.""" # We simulate the following situation: - # The temperature rises 1 °C per minute for 1 hour long. - # There is a data point every second, however, the sensor returns + # The temperature rises 1 °C per minute for 30 minutes long. + # There is a data point every 30 seconds, however, the sensor returns # the temperature rounded down to an integer value. # We use a time window of 10 minutes and therefore we can expect # (because the true derivative is 1 °C/min) an error of less than 10%. temperature_values = [] - for temperature in range(6): - temperature_values += [temperature] * 6 + for temperature in range(30): + temperature_values += [temperature] * 2 # two values per minute time_window = 600 + times = list(range(0, 1800 + 30, 30)) - times = list(range(len(temperature_values))) config, entity_id = await _setup_sensor( hass, {"time_window": {"seconds": time_window}, "unit_time": "min", "round": 1} ) # two minute window @@ -150,8 +150,8 @@ async def test_data_moving_average_for_discrete_sensor(hass): state = hass.states.get("sensor.power") derivative = round(float(state.state), config["sensor"]["round"]) # Test that the error is never more than - # (time_window_in_minutes / true_derivative * 100) = 10% - assert abs(1 - derivative) <= 0.1 + # (time_window_in_minutes / true_derivative * 100) = 10% + ε + assert abs(1 - derivative) <= 0.1 + 1e-6 async def test_prefix(hass): From 1b2f4fa19f94d555befe14656efdb8eca0b6e9b1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 12 Feb 2020 11:55:18 -0600 Subject: [PATCH 212/378] Improve Plex media_player entity naming (#31755) --- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/media_player.py | 37 ++++++++++++------- homeassistant/components/plex/server.py | 14 +++++++ tests/components/plex/mock_classes.py | 14 +++++++ 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index ad62bade1fd..635c981b531 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -3,6 +3,7 @@ from homeassistant.const import __version__ DOMAIN = "plex" NAME_FORMAT = "Plex ({})" +COMMON_PLAYERS = ["Plex Web"] DEFAULT_PORT = 32400 DEFAULT_SSL = False diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index d8155d1a43b..1e8d0e6cfd2 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -21,19 +21,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util import dt as dt_util from .const import ( + COMMON_PLAYERS, CONF_SERVER_IDENTIFIER, DISPATCHERS, DOMAIN as PLEX_DOMAIN, @@ -114,6 +109,8 @@ class PlexMediaPlayer(MediaPlayerDevice): self._is_player_active = False self._machine_identifier = device.machineIdentifier self._make = "" + self._device_product = None + self._device_title = None self._name = None self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting @@ -188,7 +185,6 @@ class PlexMediaPlayer(MediaPlayerDevice): self._clear_media_details() self._available = self.device or self.session - name_base = None if self.device: try: @@ -197,7 +193,8 @@ class PlexMediaPlayer(MediaPlayerDevice): device_url = "127.0.0.1" if "127.0.0.1" in device_url: self.device.proxyThroughServer() - name_base = self.device.title or self.device.product + self._device_product = self.device.product + self._device_title = self.device.title self._device_protocol_capabilities = self.device.protocolCapabilities self._player_state = self.device.state @@ -215,11 +212,13 @@ class PlexMediaPlayer(MediaPlayerDevice): if session_device: self._make = session_device.device or "" self._player_state = session_device.state - name_base = name_base or session_device.title or session_device.product + self._device_product = self._device_product or session_device.product + self._device_title = self._device_title or session_device.title else: _LOGGER.warning("No player associated with active session") - self._session_username = self.session.usernames[0] + if self.session.usernames: + self._session_username = self.session.usernames[0] # Calculate throttled position for proper progress display. position = int(self.session.viewOffset / 1000) @@ -237,7 +236,14 @@ class PlexMediaPlayer(MediaPlayerDevice): self._media_content_id = self.session.ratingKey self._media_content_rating = getattr(self.session, "contentRating", None) - self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME) + name_parts = [self._device_product, self._device_title] + if (self._device_product in COMMON_PLAYERS) and self.make: + # Add more context in name for likely duplicates + name_parts.append(self.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: @@ -348,6 +354,11 @@ class PlexMediaPlayer(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + 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.""" @@ -699,7 +710,7 @@ class PlexMediaPlayer(MediaPlayerDevice): """Return the scene state attributes.""" attr = { "media_content_rating": self._media_content_rating, - "session_username": self._session_username, + "session_username": self.username, "media_library_name": self._app_name, } diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index ab5d79ff81c..fcc7e5dda17 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -51,6 +51,7 @@ class PlexServer: self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) self.options = options self.server_choice = None + self._owner_username = None # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -93,6 +94,14 @@ class PlexServer: else: _connect_with_token() + owner_account = [ + account.name + for account in self._plex_server.systemAccounts() + if account.accountID == 1 + ] + if owner_account: + self._owner_username = owner_account[0] + def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" @@ -182,6 +191,11 @@ class PlexServer: """Return the plexapi PlexServer instance.""" return self._plex_server + @property + def owner(self): + """Return the Plex server owner username.""" + return self._owner_username + @property def friendly_name(self): """Return name of connected Plex server.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index de6ffa51170..ed354138cb2 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -53,6 +53,15 @@ class MockPlexAccount: return self._resources +class MockPlexSystemAccount: + """Mock a PlexSystemAccount instance.""" + + def __init__(self): + """Initialize the object.""" + self.name = "Dummy" + self.accountID = 1 + + class MockPlexServer: """Mock a PlexServer instance.""" @@ -68,6 +77,11 @@ class MockPlexServer: ] prefix = "https" if ssl else "http" self._baseurl = f"{prefix}://{host}:{port}" + self._systemAccount = MockPlexSystemAccount() + + def systemAccounts(self): + """Mock the systemAccounts lookup method.""" + return [self._systemAccount] @property def url_in_use(self): From 3e05fc1c1114182092386b2e2e8d1f0cb517c128 Mon Sep 17 00:00:00 2001 From: Emanuel Winblad Date: Wed, 12 Feb 2020 19:11:15 +0100 Subject: [PATCH 213/378] Add initial version of Vilfo Router integration (#31177) * Initial implementation of Vilfo router integration. This commit is a combination of several commits, with commit messages in bullet form below. * Added additional files to Vilfo integration. * Added generated files. * Fixed alphabetic order in generated config_flows. * Continued implementation of config flow for Vilfo integration. * Continued work on config_flow for Vilfo. * Updated requirements in manifest for Vilfo Router integration. * Some strings added to Vilfo Router integration. * Vilfo Router integration updated with sensor support. * Code style cleanup. * Additional cleanup of config flow. * Added additional UI strings for Vilfo Router * Updated tests of config flow and fixed formatting * Updated requirement upon vilfo-api-client. * Sensor refactoring including support for icons * Code style changes for Vilfo Router integration * Code cleanup * Fixed linting issues in Vilfo Router integration * Fixed import order in test for Vilfo integration. * Updates to Vilfo Router integration based on feedback Based on the feedback received, updates have been made to the Vilfo Router integration. A couple of the points mentioned have not been addressed yet, since the appropriate action has not yet been determined. These are: * https://github.com/home-assistant/home-assistant/pull/31177#discussion_r371124477 * https://github.com/home-assistant/home-assistant/pull/31177#discussion_r371202896 This commit consists of: * Removed unused folder/submodule * Fixes to __init__ * Fixes to config_flow * Fixes to const * Refactored sensors and applied fixes * Fix issue with wrong exception type in config flow * Updated tests for Vilfo integration config_flow * Updated dependency upon vilfo-api-client to improve testability * Import order fixes in test * Use constants instead of strings in tests * Updated the VilfoRouterData class to only use the hostname as unique_id when it is the default one (admin.vilfo.com). * Refactored based on feedback during review. * Changes to constant names, * Blocking IO separated to executor job, * Data for uptime sensor changed from being computed to being a timestamp, * Started refactoring uptime sensor in terms of naming and unit. * Updated constants for boot time (previously uptime) sensor. * Refactored test of Vilfo config flow to avoid patching code under test. * UI naming fixes and better exception handling. * Removed unused exception class. * Various changes to Vilfo Router integration. * Removed unit of measurement for boot time sensor, * Added support for a sensor not having a unit, * Updated the config_flow to handle when the integration is already configured, * Updated tests to avoid mocking the code under test and also to cover the aforementioned changes. * Exception handling in Vilfo Router config flow refactored to be more readable. * Refactored constant usage, fixed sensor availability and fix API client library doing I/O in async context. * Updated signature with hass first * Update call to constructor with changed order of arguments --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/vilfo/__init__.py | 125 ++++++++++++ homeassistant/components/vilfo/config_flow.py | 147 ++++++++++++++ homeassistant/components/vilfo/const.py | 36 ++++ homeassistant/components/vilfo/manifest.json | 9 + homeassistant/components/vilfo/sensor.py | 94 +++++++++ homeassistant/components/vilfo/strings.json | 23 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/vilfo/__init__.py | 1 + tests/components/vilfo/test_config_flow.py | 184 ++++++++++++++++++ 13 files changed, 630 insertions(+) create mode 100644 homeassistant/components/vilfo/__init__.py create mode 100644 homeassistant/components/vilfo/config_flow.py create mode 100644 homeassistant/components/vilfo/const.py create mode 100644 homeassistant/components/vilfo/manifest.json create mode 100644 homeassistant/components/vilfo/sensor.py create mode 100644 homeassistant/components/vilfo/strings.json create mode 100644 tests/components/vilfo/__init__.py create mode 100644 tests/components/vilfo/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index bd9bf196321..88ffa7ef150 100644 --- a/.coveragerc +++ b/.coveragerc @@ -788,6 +788,9 @@ omit = homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* + homeassistant/components/vilfo/__init__.py + homeassistant/components/vilfo/sensor.py + homeassistant/components/vilfo/const.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 48a23bba619..a96f3bb5b82 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -385,6 +385,7 @@ homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger +homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py new file mode 100644 index 00000000000..ffa628d6db2 --- /dev/null +++ b/homeassistant/components/vilfo/__init__.py @@ -0,0 +1,125 @@ +"""The Vilfo Router integration.""" +import asyncio +from datetime import timedelta +import logging + +from vilfo import Client as VilfoClient +from vilfo.exceptions import VilfoException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import Throttle + +from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST + +PLATFORMS = ["sensor"] + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the Vilfo Router component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Vilfo Router from a config entry.""" + host = entry.data[CONF_HOST] + access_token = entry.data[CONF_ACCESS_TOKEN] + + vilfo_router = VilfoRouterData(hass, host, access_token) + + await vilfo_router.async_update() + + if not vilfo_router.available: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = vilfo_router + + for platform in 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): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class VilfoRouterData: + """Define an object to hold sensor data.""" + + def __init__(self, hass, host, access_token): + """Initialize.""" + self._vilfo = VilfoClient(host, access_token) + self.hass = hass + self.host = host + self.available = False + self.firmware_version = None + self.mac_address = self._vilfo.mac + self.data = {} + self._unavailable_logged = False + + @property + def unique_id(self): + """Get the unique_id for the Vilfo Router.""" + if self.mac_address: + return self.mac_address + + if self.host == ROUTER_DEFAULT_HOST: + return self.host + + return self.host + + def _fetch_data(self): + board_information = self._vilfo.get_board_information() + load = self._vilfo.get_load() + + return { + "board_information": board_information, + "load": load, + } + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update data using calls to VilfoClient library.""" + try: + data = await self.hass.async_add_executor_job(self._fetch_data) + + self.firmware_version = data["board_information"]["version"] + self.data[ATTR_BOOT_TIME] = data["board_information"]["bootTime"] + self.data[ATTR_LOAD] = data["load"] + + self.available = True + except VilfoException as error: + if not self._unavailable_logged: + _LOGGER.error( + "Could not fetch data from %s, error: %s", self.host, error + ) + self._unavailable_logged = True + self.available = False + return + + if self.available and self._unavailable_logged: + _LOGGER.info("Vilfo Router %s is available again", self.host) + self._unavailable_logged = False diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py new file mode 100644 index 00000000000..2b9df3d9195 --- /dev/null +++ b/homeassistant/components/vilfo/config_flow.py @@ -0,0 +1,147 @@ +"""Config flow for Vilfo Router integration.""" +import ipaddress +import logging +import re + +from vilfo import Client as VilfoClient +from vilfo.exceptions import ( + AuthenticationException as VilfoAuthenticationException, + VilfoException, +) +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC + +from .const import DOMAIN # pylint:disable=unused-import +from .const import ROUTER_DEFAULT_HOST + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=ROUTER_DEFAULT_HOST): str, + vol.Required(CONF_ACCESS_TOKEN, default=""): str, + } +) + +RESULT_SUCCESS = "success" +RESULT_CANNOT_CONNECT = "cannot_connect" +RESULT_INVALID_AUTH = "invalid_auth" + + +def host_valid(host): + """Return True if hostname or IP address is valid.""" + try: + if ipaddress.ip_address(host).version == (4 or 6): + return True + except ValueError: + disallowed = re.compile(r"[^a-zA-Z\d\-]") + return all(x and not disallowed.search(x) for x in host.split(".")) + + +def _try_connect_and_fetch_basic_info(host, token): + """Attempt to connect and call the ping endpoint and, if successful, fetch basic information.""" + + # Perform the ping. This doesn't validate authentication. + controller = VilfoClient(host=host, token=token) + result = {"type": None, "data": {}} + + try: + controller.ping() + except VilfoException: + result["type"] = RESULT_CANNOT_CONNECT + result["data"] = CannotConnect + return result + + # Perform a call that requires authentication. + try: + controller.get_board_information() + except VilfoAuthenticationException: + result["type"] = RESULT_INVALID_AUTH + result["data"] = InvalidAuth + return result + + if controller.mac: + result["data"][CONF_ID] = controller.mac + result["data"][CONF_MAC] = controller.mac + else: + result["data"][CONF_ID] = host + result["data"][CONF_MAC] = None + + result["type"] = RESULT_SUCCESS + + return result + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + # Validate the host before doing anything else. + if not host_valid(data[CONF_HOST]): + raise InvalidHost + + config = {} + + result = await hass.async_add_executor_job( + _try_connect_and_fetch_basic_info, data[CONF_HOST], data[CONF_ACCESS_TOKEN] + ) + + if result["type"] != RESULT_SUCCESS: + raise result["data"] + + # Return some info we want to store in the config entry. + result_data = result["data"] + config["title"] = f"{data[CONF_HOST]}" + config[CONF_MAC] = result_data[CONF_MAC] + config[CONF_HOST] = data[CONF_HOST] + config[CONF_ID] = result_data[CONF_ID] + + return config + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Vilfo Router.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except InvalidHost: + errors[CONF_HOST] = "wrong_host" + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Unexpected exception: %s", err) + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info[CONF_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate that hostname/IP address is invalid.""" diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py new file mode 100644 index 00000000000..1a40b8430d7 --- /dev/null +++ b/homeassistant/components/vilfo/const.py @@ -0,0 +1,36 @@ +"""Constants for the Vilfo Router integration.""" +from homeassistant.const import DEVICE_CLASS_TIMESTAMP + +DOMAIN = "vilfo" + +ATTR_API_DATA_FIELD = "api_data_field" +ATTR_API_DATA_FIELD_LOAD = "load" +ATTR_API_DATA_FIELD_BOOT_TIME = "boot_time" +ATTR_DEVICE_CLASS = "device_class" +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_LOAD = "load" +ATTR_UNIT = "unit" +ATTR_BOOT_TIME = "boot_time" + +ROUTER_DEFAULT_HOST = "admin.vilfo.com" +ROUTER_DEFAULT_MODEL = "Vilfo Router" +ROUTER_DEFAULT_NAME = "Vilfo Router" +ROUTER_MANUFACTURER = "Vilfo AB" + +UNIT_PERCENT = "%" + +SENSOR_TYPES = { + ATTR_LOAD: { + ATTR_LABEL: "Load", + ATTR_UNIT: UNIT_PERCENT, + ATTR_ICON: "mdi:memory", + ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_LOAD, + }, + ATTR_BOOT_TIME: { + ATTR_LABEL: "Boot time", + ATTR_ICON: "mdi:timer", + ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_BOOT_TIME, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, +} diff --git a/homeassistant/components/vilfo/manifest.json b/homeassistant/components/vilfo/manifest.json new file mode 100644 index 00000000000..cedb485fab3 --- /dev/null +++ b/homeassistant/components/vilfo/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "vilfo", + "name": "Vilfo Router", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/vilfo", + "requirements": ["vilfo-api-client==0.3.2"], + "dependencies": [], + "codeowners": ["@ManneW"] +} diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py new file mode 100644 index 00000000000..e2909647c2d --- /dev/null +++ b/homeassistant/components/vilfo/sensor.py @@ -0,0 +1,94 @@ +"""Support for Vilfo Router sensors.""" +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_API_DATA_FIELD, + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_LABEL, + ATTR_UNIT, + DOMAIN, + ROUTER_DEFAULT_MODEL, + ROUTER_DEFAULT_NAME, + ROUTER_MANUFACTURER, + SENSOR_TYPES, +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add Vilfo Router entities from a config_entry.""" + vilfo = hass.data[DOMAIN][config_entry.entry_id] + + sensors = [] + + for sensor_type in SENSOR_TYPES: + sensors.append(VilfoRouterSensor(sensor_type, vilfo)) + + async_add_entities(sensors, True) + + +class VilfoRouterSensor(Entity): + """Define a Vilfo Router Sensor.""" + + def __init__(self, sensor_type, api): + """Initialize.""" + self.api = api + self.sensor_type = sensor_type + self._device_info = { + "identifiers": {(DOMAIN, api.host, api.mac_address)}, + "name": ROUTER_DEFAULT_NAME, + "manufacturer": ROUTER_MANUFACTURER, + "model": ROUTER_DEFAULT_MODEL, + "sw_version": api.firmware_version, + } + self._unique_id = f"{self.api.unique_id}_{self.sensor_type}" + self._state = None + + @property + def available(self): + """Return whether the sensor is available or not.""" + return self.api.available + + @property + def device_info(self): + """Return the device info.""" + return self._device_info + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_TYPES[self.sensor_type].get(ATTR_DEVICE_CLASS) + + @property + def icon(self): + """Return the icon for the sensor.""" + return SENSOR_TYPES[self.sensor_type][ATTR_ICON] + + @property + def name(self): + """Return the name of the sensor.""" + parent_device_name = self._device_info["name"] + sensor_name = SENSOR_TYPES[self.sensor_type][ATTR_LABEL] + return f"{parent_device_name} {sensor_name}" + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return SENSOR_TYPES[self.sensor_type].get(ATTR_UNIT) + + async def async_update(self): + """Update the router data.""" + await self.api.async_update() + self._state = self.api.data.get( + SENSOR_TYPES[self.sensor_type][ATTR_API_DATA_FIELD] + ) diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json new file mode 100644 index 00000000000..e7a55c55f1f --- /dev/null +++ b/homeassistant/components/vilfo/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "Vilfo Router", + "step": { + "user": { + "title": "Connect to the Vilfo Router", + "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", + "data": { + "host": "Router hostname or IP", + "access_token": "Access token for the Vilfo Router API" + } + } + }, + "error": { + "cannot_connect": "Failed to connect. Please check the information you provided and try again.", + "invalid_auth": "Invalid authentication. Please check the access token and try again.", + "unknown": "An unexpected error occurred while setting up the integration." + }, + "abort": { + "already_configured": "This Vilfo Router is already configured." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4c5449d5b2a..39a9bccf607 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -101,6 +101,7 @@ FLOWS = [ "upnp", "velbus", "vesync", + "vilfo", "vizio", "wemo", "withings", diff --git a/requirements_all.txt b/requirements_all.txt index 0dac9a0b481..303fe886716 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2032,6 +2032,9 @@ venstarcolortouch==0.12 # homeassistant.components.meteo_france vigilancemeteo==3.0.0 +# homeassistant.components.vilfo +vilfo-api-client==0.3.2 + # homeassistant.components.volkszaehler volkszaehler==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da710af4722..890bd1e96e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -688,6 +688,9 @@ uvcclient==0.11.0 # homeassistant.components.meteo_france vigilancemeteo==3.0.0 +# homeassistant.components.vilfo +vilfo-api-client==0.3.2 + # homeassistant.components.verisure vsure==1.5.4 diff --git a/tests/components/vilfo/__init__.py b/tests/components/vilfo/__init__.py new file mode 100644 index 00000000000..680b556fc12 --- /dev/null +++ b/tests/components/vilfo/__init__.py @@ -0,0 +1 @@ +"""Tests for the Vilfo Router integration.""" diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py new file mode 100644 index 00000000000..d73d15df8dd --- /dev/null +++ b/tests/components/vilfo/test_config_flow.py @@ -0,0 +1,184 @@ +"""Test the Vilfo Router config flow.""" +from unittest.mock import patch + +import vilfo + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.vilfo.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC + +from tests.common import mock_coro + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.get_board_information", return_value=None, + ), patch( + "homeassistant.components.vilfo.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.vilfo.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "testadmin.vilfo.com" + assert result2["data"] == { + "host": "testadmin.vilfo.com", + "access_token": "test-token", + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.get_board_information", + side_effect=vilfo.exceptions.AuthenticationException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "testadmin.vilfo.com", "access_token": "test-token"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "testadmin.vilfo.com", "access_token": "test-token"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "testadmin.vilfo.com", "access_token": "test-token"}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] == {"base": "cannot_connect"} + + +async def test_form_wrong_host(hass): + """Test we handle wrong host errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"host": "this is an invalid hostname", "access_token": "test-token"}, + ) + + assert result["errors"] == {"host": "wrong_host"} + + +async def test_form_already_configured(hass): + """Test that we handle already configured exceptions appropriately.""" + first_flow_result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.get_board_information", return_value=None, + ): + first_flow_result2 = await hass.config_entries.flow.async_configure( + first_flow_result1["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + + second_flow_result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.get_board_information", return_value=None, + ): + second_flow_result2 = await hass.config_entries.flow.async_configure( + second_flow_result1["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + + assert first_flow_result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert second_flow_result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert second_flow_result2["reason"] == "already_configured" + + +async def test_form_unexpected_exception(hass): + """Test that we handle unexpected exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("vilfo.Client.ping", side_effect=Exception): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "testadmin.vilfo.com", "access_token": "test-token"}, + ) + + assert result2["errors"] == {"base": "unknown"} + + +async def test_validate_input_returns_data(hass): + """Test we handle the MAC address being resolved or not.""" + mock_data = {"host": "testadmin.vilfo.com", "access_token": "test-token"} + mock_data_with_ip = {"host": "192.168.0.1", "access_token": "test-token"} + mock_mac = "FF-00-00-00-00-00" + + with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.get_board_information", return_value=None + ): + result = await hass.components.vilfo.config_flow.validate_input( + hass, data=mock_data + ) + + assert result["title"] == mock_data["host"] + assert result[CONF_HOST] == mock_data["host"] + assert result[CONF_MAC] is None + assert result[CONF_ID] == mock_data["host"] + + with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.get_board_information", return_value=None + ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac): + result2 = await hass.components.vilfo.config_flow.validate_input( + hass, data=mock_data + ) + result3 = await hass.components.vilfo.config_flow.validate_input( + hass, data=mock_data_with_ip + ) + + assert result2["title"] == mock_data["host"] + assert result2[CONF_HOST] == mock_data["host"] + assert result2[CONF_MAC] == mock_mac + assert result2[CONF_ID] == mock_mac + + assert result3["title"] == mock_data_with_ip["host"] + assert result3[CONF_HOST] == mock_data_with_ip["host"] + assert result3[CONF_MAC] == mock_mac + assert result3[CONF_ID] == mock_mac From c0756948da7ad984a2126fab6b313d40a4edc776 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 12 Feb 2020 19:12:26 +0100 Subject: [PATCH 214/378] Fix smoke detection for HomematicIP Cloud (#31753) --- homeassistant/components/homematicip_cloud/binary_sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index f16dfc986f0..52a4583be46 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -227,7 +227,11 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if smoke is detected.""" - return self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF + if self._device.smokeDetectorAlarmType: + return ( + self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF + ) + return False class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): From 3b3e062a3554ec72dbe9a1948a258e3d6261bada Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Feb 2020 10:13:07 -0800 Subject: [PATCH 215/378] Whitelist shopping list updated event (#31742) * Whitelist shopping list updated event * Add ignore to hassfest --- homeassistant/components/websocket_api/permissions.py | 10 ++++++---- script/hassfest/dependencies.py | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index c270c0f0ccc..bd6013aac0a 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -7,6 +7,7 @@ from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) +from homeassistant.components.shopping_list import EVENT as EVENT_SHOPPING_LIST_UPDATED from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, @@ -22,16 +23,17 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. SUBSCRIBE_WHITELIST = { + EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, + EVENT_DEVICE_REGISTRY_UPDATED, + EVENT_ENTITY_REGISTRY_UPDATED, + EVENT_LOVELACE_UPDATED, EVENT_PANELS_UPDATED, EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, + EVENT_SHOPPING_LIST_UPDATED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, - EVENT_AREA_REGISTRY_UPDATED, - EVENT_DEVICE_REGISTRY_UPDATED, - EVENT_ENTITY_REGISTRY_UPDATED, - EVENT_LOVELACE_UPDATED, } diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index a600eea141d..4b9dc21abb9 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -122,6 +122,7 @@ IGNORE_VIOLATIONS = { ("demo", "openalpr_local"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), + ("websocket_api", "shopping_list"), # Expose HA to external systems "homekit", "alexa", From 43256ebd8355c15c84922d3e722dbfcc865e4c85 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 12 Feb 2020 11:40:39 -0800 Subject: [PATCH 216/378] Add device name to sensor name for mobile_app (#31756) * Add device name to sensor name * Update test to include device name --- homeassistant/components/mobile_app/entity.py | 4 +++- tests/components/mobile_app/test_entity.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 27cb9934b18..5200c6b0c12 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from .const import ( + ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON, @@ -38,6 +39,7 @@ class MobileAppEntity(Entity): ) self._entity_type = config[ATTR_SENSOR_TYPE] self.unsub_dispatcher = None + self._name = f"{entry.data[ATTR_DEVICE_NAME]} {config[ATTR_SENSOR_NAME]}" async def async_added_to_hass(self): """Register callbacks.""" @@ -58,7 +60,7 @@ class MobileAppEntity(Entity): @property def name(self): """Return the name of the mobile app sensor.""" - return self._config[ATTR_SENSOR_NAME] + return self._name @property def device_class(self): diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 0db9d42048f..65dc328186d 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -35,7 +35,7 @@ async def test_sensor(hass, create_registrations, webhook_client): assert json == {"success": True} await hass.async_block_till_done() - entity = hass.states.get("sensor.battery_state") + entity = hass.states.get("sensor.test_1_battery_state") assert entity is not None assert entity.attributes["device_class"] == "battery" @@ -43,7 +43,7 @@ async def test_sensor(hass, create_registrations, webhook_client): assert entity.attributes["unit_of_measurement"] == "%" assert entity.attributes["foo"] == "bar" assert entity.domain == "sensor" - assert entity.name == "Battery State" + assert entity.name == "Test 1 Battery State" assert entity.state == "100" update_resp = await webhook_client.post( @@ -63,7 +63,7 @@ async def test_sensor(hass, create_registrations, webhook_client): assert update_resp.status == 200 - updated_entity = hass.states.get("sensor.battery_state") + updated_entity = hass.states.get("sensor.test_1_battery_state") assert updated_entity.state == "123" dev_reg = await device_registry.async_get_registry(hass) From 52fe1328f6b13a2d2844d2b465c30953020988bd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 12 Feb 2020 16:12:14 -0500 Subject: [PATCH 217/378] ZHA tests refactoring (#31744) * Refactor ZHA fixtures. Patch Zigpy radio libs instead of ZHA when setting up fixtures. Use new fixtures for binary_sensor.zha platform. * Update ZHA api tests. * Update ZHA channels and discovery tests. * Update ZHA cover tests. * Update device action/trigger tests. * Update device_tracker.zha platform tests. * Update fan.zha platform tests. * Update ZHA gateway tests. * Update lock.zha platform tests. * Update switch.zha platform tests. * Update sensor.zha platform tests. * Update light.zha platform tests. * Use MockConfigEntry. * Address PR comments. --- tests/components/zha/common.py | 64 +++++---- tests/components/zha/conftest.py | 148 ++++++++++---------- tests/components/zha/test_api.py | 6 +- tests/components/zha/test_binary_sensor.py | 18 +-- tests/components/zha/test_channels.py | 11 +- tests/components/zha/test_cover.py | 16 +-- tests/components/zha/test_device_action.py | 11 +- tests/components/zha/test_device_tracker.py | 16 +-- tests/components/zha/test_device_trigger.py | 7 +- tests/components/zha/test_discover.py | 21 +-- tests/components/zha/test_fan.py | 54 +++---- tests/components/zha/test_gateway.py | 8 +- tests/components/zha/test_light.py | 17 +-- tests/components/zha/test_lock.py | 11 +- tests/components/zha/test_sensor.py | 17 +-- tests/components/zha/test_switch.py | 14 +- 16 files changed, 187 insertions(+), 252 deletions(-) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 97668bef2ea..a9f040eda68 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -10,27 +10,10 @@ import zigpy.zcl.clusters.general import zigpy.zcl.foundation as zcl_f import zigpy.zdo.types -from homeassistant.components.zha.core.const import ( - DATA_ZHA, - DATA_ZHA_BRIDGE_ID, - DATA_ZHA_CONFIG, - DATA_ZHA_DISPATCHERS, -) +import homeassistant.components.zha.core.const as zha_const from homeassistant.util import slugify -class FakeApplication: - """Fake application for mocking zigpy.""" - - def __init__(self): - """Init fake application.""" - self.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") - self.nwk = 0x087D - - -APPLICATION = FakeApplication() - - class FakeEndpoint: """Fake endpoint for moking zigpy.""" @@ -71,14 +54,15 @@ def patch_cluster(cluster): cluster.read_attributes = CoroutineMock() cluster.read_attributes_raw = Mock() cluster.unbind = CoroutineMock(return_value=[0]) + cluster.write_attributes = CoroutineMock(return_value=[0]) class FakeDevice: """Fake device for mocking zigpy.""" - def __init__(self, ieee, manufacturer, model, node_desc=None): + def __init__(self, app, ieee, manufacturer, model, node_desc=None): """Init fake device.""" - self._application = APPLICATION + self._application = app self.ieee = zigpy.types.EUI64.convert(ieee) self.nwk = 0xB79C self.zdo = Mock() @@ -98,6 +82,14 @@ class FakeDevice: self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0] +def get_zha_gateway(hass): + """Return ZHA gateway from hass.data.""" + try: + return hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + except KeyError: + return None + + def make_attribute(attrid, value, status=0): """Make an attribute.""" attr = zcl_f.Attribute() @@ -107,14 +99,6 @@ def make_attribute(attrid, value, status=0): return attr -async def async_setup_entry(hass, config_entry): - """Mock setup entry for zha.""" - hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = {} - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] - hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = APPLICATION.ieee - return True - - async def find_entity_id(domain, zha_device, hass): """Find the entity id under the testing. @@ -133,7 +117,7 @@ async def find_entity_id(domain, zha_device, hass): return None -async def async_enable_traffic(hass, zha_gateway, zha_devices): +async def async_enable_traffic(hass, zha_devices): """Allow traffic to flow through the gateway and the zha device.""" for zha_device in zha_devices: zha_device.update_available(True) @@ -147,3 +131,25 @@ def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHe else: frc = zcl_f.FrameControl(zcl_f.FrameType.CLUSTER_COMMAND) return zcl_f.ZCLHeader(frc, tsn=1, command_id=command_id) + + +def reset_clusters(clusters): + """Reset mocks on cluster.""" + for cluster in clusters: + cluster.bind.reset_mock() + cluster.configure_reporting.reset_mock() + cluster.write_attributes.reset_mock() + + +async def async_test_rejoin(hass, zigpy_device, clusters, report_counts, ep_id=1): + """Test device rejoins.""" + reset_clusters(clusters) + + zha_gateway = get_zha_gateway(hass) + await zha_gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done() + for cluster, reports in zip(clusters, report_counts): + assert cluster.bind.call_count == 1 + assert cluster.bind.await_count == 1 + assert cluster.configure_reporting.call_count == reports + assert cluster.configure_reporting.await_count == reports diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index f54c6ca8602..26dd2b5da5c 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,19 +1,18 @@ """Test configuration for the ZHA component.""" -import functools from unittest import mock -from unittest.mock import patch import asynctest import pytest import zigpy from zigpy.application import ControllerApplication +import zigpy.group +import zigpy.types -from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN -from homeassistant.components.zha.core.gateway import ZHAGateway -from homeassistant.components.zha.core.store import async_get_registry -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +import homeassistant.components.zha.core.const as zha_const +import homeassistant.components.zha.core.registries as zha_regs +from homeassistant.setup import async_setup_component -from .common import FakeDevice, FakeEndpoint, async_setup_entry +from .common import FakeDevice, FakeEndpoint, get_zha_gateway from tests.common import MockConfigEntry @@ -21,52 +20,64 @@ FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" -@pytest.fixture(name="config_entry") -async def config_entry_fixture(hass): - """Fixture representing a config entry.""" - config_entry = MockConfigEntry(domain=DOMAIN) - config_entry.add_to_hass(hass) - return config_entry +@pytest.fixture +def zigpy_app_controller(): + """Zigpy ApplicationController fixture.""" + app = mock.MagicMock(spec_set=ControllerApplication) + app.startup = asynctest.CoroutineMock() + app.shutdown = asynctest.CoroutineMock() + groups = zigpy.group.Groups(app) + groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) + app.configure_mock(groups=groups) + type(app).ieee = mock.PropertyMock() + app.ieee.return_value = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") + type(app).nwk = mock.PropertyMock(return_value=zigpy.types.NWK(0x0000)) + type(app).devices = mock.PropertyMock(return_value={}) + return app @pytest.fixture -async def setup_zha(hass, config_entry): - """Load the ZHA component. - - This will init the ZHA component. It loads the component in HA so that - we can test the domains that ZHA supports without actually having a zigbee - network running. - """ - # this prevents needing an actual radio and zigbee network available - with patch("homeassistant.components.zha.async_setup_entry", async_setup_entry): - hass.data[DATA_ZHA] = {} - - # init ZHA - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - await hass.async_block_till_done() +def zigpy_radio(): + """Zigpy radio mock.""" + radio = mock.MagicMock() + radio.connect = asynctest.CoroutineMock() + return radio -@pytest.fixture(name="zha_gateway") -async def zha_gateway_fixture(hass, config_entry, setup_zha): - """Fixture representing a zha gateway. +@pytest.fixture(name="config_entry") +async def config_entry_fixture(hass): + """Fixture representing a config entry.""" + entry = MockConfigEntry( + version=1, + domain=zha_const.DOMAIN, + data={ + zha_const.CONF_BAUDRATE: zha_const.DEFAULT_BAUDRATE, + zha_const.CONF_RADIO_TYPE: "MockRadio", + zha_const.CONF_USB_PATH: "/dev/ttyUSB0", + }, + ) + entry.add_to_hass(hass) + return entry - Create a ZHAGateway object that can be used to interact with as if we - had a real zigbee network running. - """ - for component in COMPONENTS: - hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) - zha_storage = await async_get_registry(hass) - dev_reg = await get_dev_reg(hass) - gateway = ZHAGateway(hass, {}, config_entry) - gateway.zha_storage = zha_storage - gateway.ha_device_registry = dev_reg - gateway.application_controller = mock.MagicMock(spec_set=ControllerApplication) - groups = zigpy.group.Groups(gateway.application_controller) - groups.add_listener(gateway) - groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) - gateway.application_controller.configure_mock(groups=groups) - gateway._initialize_groups() - return gateway + +@pytest.fixture +def setup_zha(hass, config_entry, zigpy_app_controller, zigpy_radio): + """Set up ZHA component.""" + zha_config = {zha_const.DOMAIN: {zha_const.CONF_ENABLE_QUIRKS: False}} + + radio_details = { + zha_const.ZHA_GW_RADIO: mock.MagicMock(return_value=zigpy_radio), + zha_const.CONTROLLER: mock.MagicMock(return_value=zigpy_app_controller), + zha_const.ZHA_GW_RADIO_DESCRIPTION: "mock radio", + } + + async def _setup(): + with mock.patch.dict(zha_regs.RADIO_TYPES, {"MockRadio": radio_details}): + status = await async_setup_component(hass, zha_const.DOMAIN, zha_config) + assert status is True + await hass.async_block_till_done() + + return _setup @pytest.fixture @@ -86,7 +97,7 @@ def channel(): @pytest.fixture -def zigpy_device_mock(): +def zigpy_device_mock(zigpy_app_controller): """Make a fake device using the specified cluster classes.""" def _mock_dev( @@ -94,10 +105,12 @@ def zigpy_device_mock(): ieee="00:0d:6f:00:0a:90:69:e7", manufacturer="FakeManufacturer", model="FakeModel", - node_desc=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", + node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", ): """Make a fake device using the specified cluster classes.""" - device = FakeDevice(ieee, manufacturer, model, node_desc) + device = FakeDevice( + zigpy_app_controller, ieee, manufacturer, model, node_descriptor + ) for epid, ep in endpoints.items(): endpoint = FakeEndpoint(manufacturer, model, epid) endpoint.device = device @@ -119,19 +132,13 @@ def zigpy_device_mock(): @pytest.fixture -def _zha_device_restored_or_joined(hass, zha_gateway, config_entry): - """Make a restored or joined ZHA devices.""" +def zha_device_joined(hass, setup_zha): + """Return a newly joined ZHA device.""" - async def _zha_device(is_new_join, zigpy_dev): - if is_new_join: - for cmp in COMPONENTS: - await hass.config_entries.async_forward_entry_setup(config_entry, cmp) - await hass.async_block_till_done() - await zha_gateway.async_device_initialized(zigpy_dev) - else: - await zha_gateway.async_device_restored(zigpy_dev) - for cmp in COMPONENTS: - await hass.config_entries.async_forward_entry_setup(config_entry, cmp) + async def _zha_device(zigpy_dev): + await setup_zha() + zha_gateway = get_zha_gateway(hass) + await zha_gateway.async_device_initialized(zigpy_dev) await hass.async_block_till_done() return zha_gateway.get_device(zigpy_dev.ieee) @@ -139,17 +146,16 @@ def _zha_device_restored_or_joined(hass, zha_gateway, config_entry): @pytest.fixture -def zha_device_joined(_zha_device_restored_or_joined): - """Return a newly joined ZHA device.""" - - return functools.partial(_zha_device_restored_or_joined, True) - - -@pytest.fixture -def zha_device_restored(_zha_device_restored_or_joined): +def zha_device_restored(hass, zigpy_app_controller, setup_zha): """Return a restored ZHA device.""" - return functools.partial(_zha_device_restored_or_joined, False) + async def _zha_device(zigpy_dev): + zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev + await setup_zha() + zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + return zha_gateway.get_device(zigpy_dev.ieee) + + return _zha_device @pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index c61ecfa8c71..b67a39cd3ab 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -28,7 +28,7 @@ IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @pytest.fixture -async def device_switch(hass, zha_gateway, zigpy_device_mock, zha_device_joined): +async def device_switch(hass, zigpy_device_mock, zha_device_joined): """Test zha switch platform.""" zigpy_device = zigpy_device_mock( @@ -47,7 +47,7 @@ async def device_switch(hass, zha_gateway, zigpy_device_mock, zha_device_joined) @pytest.fixture -async def device_groupable(hass, zha_gateway, zigpy_device_mock, zha_device_joined): +async def device_groupable(hass, zigpy_device_mock, zha_device_joined): """Test zha light platform.""" zigpy_device = zigpy_device_mock( @@ -78,7 +78,7 @@ async def zha_client(hass, hass_ws_client, device_switch, device_groupable): return await hass_ws_client(hass) -async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): +async def test_device_clusters(hass, zha_client): """Test getting device cluster info.""" await zha_client.send_json( {ID: 5, TYPE: "zha/devices/clusters", ATTR_IEEE: IEEE_SWITCH_DEVICE} diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 4be5f3c1f39..a22bfa54dae 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, @@ -65,13 +66,12 @@ async def async_test_iaszone_on_off(hass, cluster, entity_id): @pytest.mark.parametrize( "device, on_off_test, cluster_name, reporting", [ - (DEVICE_IAS, async_test_iaszone_on_off, "ias_zone", False), - (DEVICE_OCCUPANCY, async_test_binary_sensor_on_off, "occupancy", True), + (DEVICE_IAS, async_test_iaszone_on_off, "ias_zone", (0,)), + (DEVICE_OCCUPANCY, async_test_binary_sensor_on_off, "occupancy", (1,)), ], ) async def test_binary_sensor( hass, - zha_gateway, zigpy_device_mock, zha_device_joined_restored, device, @@ -89,7 +89,7 @@ async def test_binary_sensor( # test that the sensors exist and are in the unavailable state assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the sensors exist and are in the off state assert hass.states.get(entity_id).state == STATE_OFF @@ -99,13 +99,5 @@ async def test_binary_sensor( await on_off_test(hass, cluster, entity_id) # test rejoin - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_device) - await hass.async_block_till_done() + await async_test_rejoin(hass, zigpy_device, [cluster], reporting) assert hass.states.get(entity_id).state == STATE_OFF - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - if reporting: - assert cluster.configure_reporting.call_count > 0 - assert cluster.configure_reporting.await_count > 0 diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index c5ad4d3fbc0..ee493ca01a7 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -6,6 +6,8 @@ import homeassistant.components.zha.core.channels as channels import homeassistant.components.zha.core.device as zha_device import homeassistant.components.zha.core.registries as registries +from .common import get_zha_gateway + @pytest.fixture def ieee(): @@ -19,6 +21,13 @@ def nwk(): return t.NWK(0xBEEF) +@pytest.fixture +async def zha_gateway(hass, setup_zha): + """Return ZhaGateway fixture.""" + await setup_zha() + return get_zha_gateway(hass) + + @pytest.mark.parametrize( "cluster_id, bind_count, attrs", [ @@ -63,7 +72,7 @@ def nwk(): ], ) async def test_in_channel_config( - cluster_id, bind_count, attrs, zha_gateway, hass, zigpy_device_mock + cluster_id, bind_count, attrs, hass, zigpy_device_mock, zha_gateway ): """Test ZHA core channel configuration for input clusters.""" zigpy_dev = zigpy_device_mock( diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 321ad95e73e..e5883605e34 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -12,6 +12,7 @@ from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, @@ -37,9 +38,7 @@ def zigpy_cover_device(zigpy_device_mock): @asynctest.patch( "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" ) -async def test_cover( - m1, hass, zha_gateway, zha_device_joined_restored, zigpy_cover_device -): +async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): """Test zha cover platform.""" async def get_chan_attr(*args, **kwargs): @@ -62,7 +61,7 @@ async def test_cover( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) await hass.async_block_till_done() attr = make_attribute(8, 100) @@ -132,12 +131,5 @@ async def test_cover( ) # test rejoin - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_cover_device) - await hass.async_block_till_done() + await async_test_rejoin(hass, zigpy_cover_device, [cluster], (1,)) assert hass.states.get(entity_id).state == STATE_OPEN - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - assert cluster.configure_reporting.call_count == 1 - assert cluster.configure_reporting.await_count == 1 diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 4f41878f952..8866e6cff55 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -23,13 +23,7 @@ COMMAND_SINGLE = "single" @pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "zha", "warning_device_warn") - - -@pytest.fixture -async def device_ias(hass, zha_gateway, zigpy_device_mock, zha_device_joined_restored): +async def device_ias(hass, zigpy_device_mock, zha_device_joined_restored): """IAS device fixture.""" clusters = [general.Basic, security.IasZone, security.IasWd] @@ -67,7 +61,7 @@ async def test_get_actions(hass, device_ias): assert actions == expected_actions -async def test_action(hass, calls, device_ias): +async def test_action(hass, device_ias): """Test for executing a zha device action.""" zigpy_device, zha_device = device_ias @@ -108,6 +102,7 @@ async def test_action(hass, calls, device_ias): ) await hass.async_block_till_done() + calls = async_mock_service(hass, DOMAIN, "warning_device_warn") channel = {ch.name: ch for ch in zha_device.all_channels}[CHANNEL_EVENT_RELAY] channel.zha_send_event(channel.cluster, COMMAND_SINGLE, []) diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index fe5661c4776..3782cdc09a7 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -15,6 +15,7 @@ import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, @@ -42,9 +43,7 @@ def zigpy_device_dt(zigpy_device_mock): return zigpy_device_mock(endpoints) -async def test_device_tracker( - hass, zha_gateway, zha_device_joined_restored, zigpy_device_dt -): +async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt): """Test zha device tracker platform.""" zha_device = await zha_device_joined_restored(zigpy_device_dt) @@ -61,7 +60,7 @@ async def test_device_tracker( await hass.async_block_till_done() # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the state has changed from unavailable to not home assert hass.states.get(entity_id).state == STATE_NOT_HOME @@ -88,12 +87,5 @@ async def test_device_tracker( assert entity.battery_level == 100 # test adding device tracker to the network and HA - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_device_dt) - await hass.async_block_till_done() + await async_test_rejoin(hass, zigpy_device_dt, [cluster], (2,)) assert hass.states.get(entity_id).state == STATE_HOME - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - assert cluster.configure_reporting.call_count == 2 - assert cluster.configure_reporting.await_count == 2 diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 56f5c6c85ba..4bb7567d1e6 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -39,8 +39,8 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -@pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) -async def mock_devices(hass, zha_gateway, zigpy_device_mock, request): +@pytest.fixture +async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored): """IAS device fixture.""" zigpy_device = zigpy_device_mock( @@ -53,8 +53,7 @@ async def mock_devices(hass, zha_gateway, zigpy_device_mock, request): }, ) - join_or_restore = request.getfixturevalue(request.param) - zha_device = await join_or_restore(zigpy_device) + zha_device = await zha_device_joined_restored(zigpy_device) zha_device.update_available(True) await hass.async_block_till_done() return zigpy_device, zha_device diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 9ed88c86e51..a194453bd65 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1,6 +1,5 @@ """Test zha device discovery.""" -import asyncio import re from unittest import mock @@ -11,6 +10,7 @@ import homeassistant.components.zha.core.discovery as disc import homeassistant.components.zha.core.gateway as core_zha_gw import homeassistant.helpers.entity_registry +from .common import get_zha_gateway from .zha_devices_list import DEVICES NO_TAIL_ID = re.compile("_\\d$") @@ -18,12 +18,7 @@ NO_TAIL_ID = re.compile("_\\d$") @pytest.mark.parametrize("device", DEVICES) async def test_devices( - device, - zha_gateway: core_zha_gw.ZHAGateway, - hass, - config_entry, - zigpy_device_mock, - monkeypatch, + device, hass, zigpy_device_mock, monkeypatch, zha_device_joined_restored ): """Test device discovery.""" @@ -32,7 +27,7 @@ async def test_devices( "00:11:22:33:44:55:66:77", device["manufacturer"], device["model"], - node_desc=device["node_descriptor"], + node_descriptor=device["node_descriptor"], ) _dispatch = mock.MagicMock(wraps=disc.async_dispatch_discovery_info) @@ -45,14 +40,7 @@ async def test_devices( "homeassistant.components.zha.core.discovery._async_create_cluster_channel", wraps=disc._async_create_cluster_channel, ): - await zha_gateway.async_device_restored(zigpy_device) - await hass.async_block_till_done() - tasks = [ - hass.config_entries.async_forward_entry_setup(config_entry, component) - for component in zha_const.COMPONENTS - ] - await asyncio.gather(*tasks) - + await zha_device_joined_restored(zigpy_device) await hass.async_block_till_done() entity_ids = hass.states.async_entity_ids() @@ -61,6 +49,7 @@ async def test_devices( ent for ent in entity_ids if ent.split(".")[0] in zha_const.COMPONENTS } + zha_gateway = get_zha_gateway(hass) zha_dev = zha_gateway.get_device(zigpy_device.ieee) event_channels = { # pylint: disable=protected-access ch.id for ch in zha_dev._relay_channels.values() diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 48222993a3a..0cf3e3e954d 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,5 +1,5 @@ """Test zha fan.""" -from unittest.mock import call, patch +from unittest.mock import call import pytest import zigpy.zcl.clusters.hvac as hvac @@ -18,13 +18,12 @@ from homeassistant.const import ( from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, ) -from tests.common import mock_coro - @pytest.fixture def zigpy_device(zigpy_device_mock): @@ -35,7 +34,7 @@ def zigpy_device(zigpy_device_mock): return zigpy_device_mock(endpoints) -async def test_fan(hass, zha_gateway, zha_device_joined_restored, zigpy_device): +async def test_fan(hass, zha_device_joined_restored, zigpy_device): """Test zha fan platform.""" zha_device = await zha_device_joined_restored(zigpy_device) @@ -47,7 +46,7 @@ async def test_fan(hass, zha_gateway, zha_device_joined_restored, zigpy_device): assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the state has changed from unavailable to off assert hass.states.get(entity_id).state == STATE_OFF @@ -66,44 +65,25 @@ async def test_fan(hass, zha_gateway, zha_device_joined_restored, zigpy_device): assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), - ): - # turn on via UI - await async_turn_on(hass, entity_id) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 2}) + cluster.write_attributes.reset_mock() + await async_turn_on(hass, entity_id) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 2}) # turn off from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), - ): - # turn off via UI - await async_turn_off(hass, entity_id) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 0}) + cluster.write_attributes.reset_mock() + await async_turn_off(hass, entity_id) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 0}) # change speed from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), - ): - # turn on via UI - await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 3}) + cluster.write_attributes.reset_mock() + await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_device) - await hass.async_block_till_done() - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - assert cluster.configure_reporting.call_count == 1 - assert cluster.configure_reporting.await_count == 1 + await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 45f47a1fa87..74aed6f5872 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -2,7 +2,7 @@ import pytest import zigpy.zcl.clusters.general as general -from .common import async_enable_traffic +from .common import async_enable_traffic, get_zha_gateway @pytest.fixture @@ -27,13 +27,13 @@ async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): return zha_device -async def test_device_left(hass, zha_gateway, zigpy_dev_basic, zha_dev_basic): +async def test_device_left(hass, zigpy_dev_basic, zha_dev_basic): """Device leaving the network should become unavailable.""" assert zha_dev_basic.available is False - await async_enable_traffic(hass, zha_gateway, [zha_dev_basic]) + await async_enable_traffic(hass, [zha_dev_basic]) assert zha_dev_basic.available is True - zha_gateway.device_left(zigpy_dev_basic) + get_zha_gateway(hass).device_left(zigpy_dev_basic) assert zha_dev_basic.available is False diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 5df379d1d7a..e21c22d30cf 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,6 +14,7 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, @@ -73,7 +74,7 @@ LIGHT_COLOR = { [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))], ) async def test_light( - hass, zha_gateway, zigpy_device_mock, zha_device_joined_restored, device, reporting, + hass, zigpy_device_mock, zha_device_joined_restored, device, reporting, ): """Test zha light platform.""" @@ -92,7 +93,7 @@ async def test_light( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the lights were created and are off assert hass.states.get(entity_id).state == STATE_OFF @@ -121,17 +122,7 @@ async def test_light( clusters.append(cluster_level) if cluster_color: clusters.append(cluster_color) - for cluster in clusters: - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_device) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF - for cluster, reporting_count in zip(clusters, reporting): - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - assert cluster.configure_reporting.call_count == reporting_count - assert cluster.configure_reporting.await_count == reporting_count + await async_test_rejoin(hass, zigpy_device, clusters, reporting) async def async_test_on_off_from_light(hass, cluster, entity_id): diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 2c5dc9f41ba..0442ea497d7 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -23,8 +23,8 @@ LOCK_DOOR = 0 UNLOCK_DOOR = 1 -@pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) -async def lock(hass, zha_gateway, zigpy_device_mock, request): +@pytest.fixture +async def lock(hass, zigpy_device_mock, zha_device_joined_restored): """Lock cluster fixture.""" zigpy_device = zigpy_device_mock( @@ -37,12 +37,11 @@ async def lock(hass, zha_gateway, zigpy_device_mock, request): }, ) - join_or_restore = request.getfixturevalue(request.param) - zha_device = await join_or_restore(zigpy_device) + zha_device = await zha_device_joined_restored(zigpy_device) return zha_device, zigpy_device.endpoints[1].door_lock -async def test_lock(hass, zha_gateway, lock): +async def test_lock(hass, lock): """Test zha lock platform.""" zha_device, cluster = lock @@ -53,7 +52,7 @@ async def test_lock(hass, zha_gateway, lock): assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the state has changed from unavailable to unlocked assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index b78ea8a3583..b81e8f02c12 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -25,6 +25,7 @@ from homeassistant.util import dt as dt_util from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, @@ -102,7 +103,6 @@ async def async_test_electrical_measurement(hass, cluster, entity_id): ) async def test_sensor( hass, - zha_gateway, zigpy_device_mock, zha_device_joined_restored, cluster_id, @@ -128,7 +128,7 @@ async def test_sensor( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and devices - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the sensor now have a state of unknown assert hass.states.get(entity_id).state == STATE_UNKNOWN @@ -137,15 +137,7 @@ async def test_sensor( await test_func(hass, cluster, entity_id) # test rejoin - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_device) - await hass.async_block_till_done() - await test_func(hass, cluster, entity_id) - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - assert cluster.configure_reporting.call_count == report_count - assert cluster.configure_reporting.await_count == report_count + await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,)) async def send_attribute_report(hass, cluster, attrid, value): @@ -232,7 +224,6 @@ async def test_temp_uom( expected, restore, hass_ms, - zha_gateway, core_rs, zigpy_device_mock, zha_device_restored, @@ -267,7 +258,7 @@ async def test_temp_uom( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and devices - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the sensors now have a state of unknown if not restore: diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index f70538f65e8..a088283834b 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -10,6 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, + async_test_rejoin, find_entity_id, make_attribute, make_zcl_header, @@ -34,7 +35,7 @@ def zigpy_device(zigpy_device_mock): return zigpy_device_mock(endpoints) -async def test_switch(hass, zha_gateway, zha_device_joined_restored, zigpy_device): +async def test_switch(hass, zha_device_joined_restored, zigpy_device): """Test zha switch platform.""" zha_device = await zha_device_joined_restored(zigpy_device) @@ -46,7 +47,7 @@ async def test_switch(hass, zha_gateway, zha_device_joined_restored, zigpy_devic assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_gateway, [zha_device]) + await async_enable_traffic(hass, [zha_device]) # test that the state has changed from unavailable to off assert hass.states.get(entity_id).state == STATE_OFF @@ -93,11 +94,4 @@ async def test_switch(hass, zha_gateway, zha_device_joined_restored, zigpy_devic ) # test joining a new switch to the network and HA - cluster.bind.reset_mock() - cluster.configure_reporting.reset_mock() - await zha_gateway.async_device_initialized(zigpy_device) - await hass.async_block_till_done() - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - assert cluster.configure_reporting.call_count == 1 - assert cluster.configure_reporting.await_count == 1 + await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) From 56316b1f6e3afc3134eb9e5ede52f9c6d8999a35 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 12 Feb 2020 23:53:26 +0100 Subject: [PATCH 218/378] Updated frontend to 20200130.3 (#31771) --- 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 8c54d20429c..10d2884f182 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200130.2" + "home-assistant-frontend==20200130.3" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c638f6e2fa7..b3cc3ab25dd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200130.2 +home-assistant-frontend==20200130.3 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 303fe886716..a7365f9f1f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -683,7 +683,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.2 +home-assistant-frontend==20200130.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 890bd1e96e9..30012bf03fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.2 +home-assistant-frontend==20200130.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From 834acd85d3715562124fa0cf200d8030999c7c8d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 12 Feb 2020 23:54:33 +0100 Subject: [PATCH 219/378] Updated frontend to 20200212.0 (#31772) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 10d2884f182..750c37bc1cd 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20200130.3" - ], + "requirements": ["home-assistant-frontend==20200212.0"], "dependencies": [ "api", "auth", @@ -15,8 +13,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b3cc3ab25dd..e18f89e3cb7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200130.3 +home-assistant-frontend==20200212.0 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index a7365f9f1f8..7abd76ab175 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -683,7 +683,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.3 +home-assistant-frontend==20200212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30012bf03fe..10acc321d3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200130.3 +home-assistant-frontend==20200212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From 6879105b141ef788a9d6b15732c37f6981273659 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Feb 2020 17:35:07 -0600 Subject: [PATCH 220/378] Cleanup August activity processing and add tests (#31774) * Update py-august to 0.12.0 * Upstream update also resolves issue #28960 --- .../components/august/binary_sensor.py | 27 ++-- homeassistant/components/august/camera.py | 4 +- homeassistant/components/august/lock.py | 29 ++-- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 95 +++++++++++++ tests/components/august/test_binary_sensor.py | 113 +++++++++++++++ tests/components/august/test_init.py | 25 +--- tests/components/august/test_lock.py | 131 ++++++++++++++++++ 10 files changed, 378 insertions(+), 52 deletions(-) create mode 100644 tests/components/august/mocks.py create mode 100644 tests/components/august/test_binary_sensor.py create mode 100644 tests/components/august/test_lock.py diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 0984eb0629f..10b3e98bb2e 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import logging -from august.activity import ActivityType +from august.activity import ACTIVITY_ACTION_STATES, ActivityType from august.lock import LockDoorStatus from homeassistant.components.binary_sensor import BinarySensorDevice @@ -138,12 +138,12 @@ class AugustDoorBinarySensor(BinarySensorDevice): self._state = self._state == LockDoorStatus.OPEN - activity = self._data.get_latest_device_activity( + door_activity = self._data.get_latest_device_activity( self._door.device_id, ActivityType.DOOR_OPERATION ) - if activity is not None: - self._sync_door_activity(activity) + if door_activity is not None: + self._sync_door_activity(door_activity) def _update_door_state(self, door_state, update_start_time): new_state = door_state == LockDoorStatus.OPEN @@ -153,7 +153,7 @@ class AugustDoorBinarySensor(BinarySensorDevice): self._door.device_id, door_state, update_start_time ) - def _sync_door_activity(self, activity): + def _sync_door_activity(self, door_activity): """Check the activity for the latest door open/close activity (events). We use this to determine the door state in between calls to the lock @@ -162,25 +162,26 @@ class AugustDoorBinarySensor(BinarySensorDevice): last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc( self._door.device_id ) - activity_end_time_utc = dt.as_utc(activity.activity_end_time) + activity_end_time_utc = dt.as_utc(door_activity.activity_end_time) if activity_end_time_utc > last_door_state_update_time_utc: _LOGGER.debug( "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]", self.name, - activity.action, + door_activity.action, activity_end_time_utc, last_door_state_update_time_utc, ) - activity_start_time_utc = dt.as_utc(activity.activity_start_time) - if activity.action == "doorclosed": - self._update_door_state(LockDoorStatus.CLOSED, activity_start_time_utc) - elif activity.action == "dooropen": - self._update_door_state(LockDoorStatus.OPEN, activity_start_time_utc) + activity_start_time_utc = dt.as_utc(door_activity.activity_start_time) + if door_activity.action in ACTIVITY_ACTION_STATES: + self._update_door_state( + ACTIVITY_ACTION_STATES[door_activity.action], + activity_start_time_utc, + ) else: _LOGGER.info( "Unhandled door activity action %s for %s", - activity.action, + door_activity.action, self.name, ) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 885ee444c6b..aa23d9b7874 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -22,10 +22,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AugustCamera(Camera): - """An implementation of a Canary security camera.""" + """An implementation of a August security camera.""" def __init__(self, data, doorbell, timeout): - """Initialize a Canary security camera.""" + """Initialize a August security camera.""" super().__init__() self._data = data self._doorbell = doorbell diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 908e20e68ca..4e9f5191b2c 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from august.activity import ActivityType +from august.activity import ACTIVITY_ACTION_STATES, ActivityType from august.lock import LockStatus from homeassistant.components.lock import LockDevice @@ -67,15 +67,15 @@ class AugustLock(LockDevice): self._lock_detail = self._data.get_lock_detail(self._lock.device_id) - activity = self._data.get_latest_device_activity( + lock_activity = self._data.get_latest_device_activity( self._lock.device_id, ActivityType.LOCK_OPERATION ) - if activity is not None: - self._changed_by = activity.operated_by - self._sync_lock_activity(activity) + if lock_activity is not None: + self._changed_by = lock_activity.operated_by + self._sync_lock_activity(lock_activity) - def _sync_lock_activity(self, activity): + def _sync_lock_activity(self, lock_activity): """Check the activity for the latest lock/unlock activity (events). We use this to determine the lock state in between calls to the lock @@ -84,25 +84,26 @@ class AugustLock(LockDevice): last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc( self._lock.device_id ) - activity_end_time_utc = dt.as_utc(activity.activity_end_time) + activity_end_time_utc = dt.as_utc(lock_activity.activity_end_time) if activity_end_time_utc > last_lock_status_update_time_utc: _LOGGER.debug( "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]", self.name, - activity.action, + lock_activity.action, activity_end_time_utc, last_lock_status_update_time_utc, ) - activity_start_time_utc = dt.as_utc(activity.activity_start_time) - if activity.action == "lock" or activity.action == "onetouchlock": - self._update_lock_status(LockStatus.LOCKED, activity_start_time_utc) - elif activity.action == "unlock": - self._update_lock_status(LockStatus.UNLOCKED, activity_start_time_utc) + activity_start_time_utc = dt.as_utc(lock_activity.activity_start_time) + if lock_activity.action in ACTIVITY_ACTION_STATES: + self._update_lock_status( + ACTIVITY_ACTION_STATES[lock_activity.action], + activity_start_time_utc, + ) else: _LOGGER.info( "Unhandled lock activity action %s for %s", - activity.action, + lock_activity.action, self.name, ) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fb5bb3ef3ef..9b4cceccd42 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.11.0"], + "requirements": ["py-august==0.12.0"], "dependencies": ["configurator"], "codeowners": ["@bdraco"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7abd76ab175..6a1699d88fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1074,7 +1074,7 @@ pushover_complete==1.1.1 pwmled==1.4.1 # homeassistant.components.august -py-august==0.11.0 +py-august==0.12.0 # homeassistant.components.canary py-canary==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10acc321d3a..130f9c4530d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,7 +390,7 @@ pure-python-adb==0.2.2.dev0 pushbullet.py==0.11.0 # homeassistant.components.august -py-august==0.11.0 +py-august==0.12.0 # homeassistant.components.canary py-canary==0.5.0 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py new file mode 100644 index 00000000000..7f2f9e0882b --- /dev/null +++ b/tests/components/august/mocks.py @@ -0,0 +1,95 @@ +"""Mocks for the august component.""" +import datetime +from unittest.mock import MagicMock, PropertyMock + +from august.activity import Activity + +from homeassistant.components.august import AugustData +from homeassistant.util import dt + + +class MockActivity(Activity): + """A mock for py-august Activity class.""" + + def __init__( + self, action=None, activity_start_timestamp=None, activity_end_timestamp=None + ): + """Init the py-august Activity class mock.""" + self._action = action + self._activity_start_timestamp = activity_start_timestamp + self._activity_end_timestamp = activity_end_timestamp + + @property + def activity_start_time(self): + """Mock the time activity started.""" + return datetime.datetime.fromtimestamp(self._activity_start_timestamp) + + @property + def activity_end_time(self): + """Mock the time activity ended.""" + return datetime.datetime.fromtimestamp(self._activity_end_timestamp) + + @property + def action(self): + """Mock the action.""" + return self._action + + +class MockAugustData(AugustData): + """A wrapper to mock AugustData.""" + + # AugustData support multiple locks, however for the purposes of + # mocking we currently only mock one lockid + + def __init__( + self, last_lock_status_update_timestamp=1, last_door_state_update_timestamp=1 + ): + """Mock AugustData.""" + self._last_lock_status_update_time_utc = dt.as_utc( + datetime.datetime.fromtimestamp(last_lock_status_update_timestamp) + ) + self._last_door_state_update_time_utc = dt.as_utc( + datetime.datetime.fromtimestamp(last_lock_status_update_timestamp) + ) + + def get_last_lock_status_update_time_utc(self, device_id): + """Mock to get last lock status update time.""" + return self._last_lock_status_update_time_utc + + def set_last_lock_status_update_time_utc(self, device_id, update_time): + """Mock to set last lock status update time.""" + self._last_lock_status_update_time_utc = update_time + + def get_last_door_state_update_time_utc(self, device_id): + """Mock to get last door state update time.""" + return self._last_door_state_update_time_utc + + def set_last_door_state_update_time_utc(self, device_id, update_time): + """Mock to set last door state update time.""" + self._last_door_state_update_time_utc = update_time + + +def _mock_august_lock(): + lock = MagicMock(name="august.lock") + type(lock).device_id = PropertyMock(return_value="lock_device_id_1") + return lock + + +def _mock_august_authenticator(): + authenticator = MagicMock(name="august.authenticator") + authenticator.should_refresh = MagicMock( + name="august.authenticator.should_refresh", return_value=0 + ) + authenticator.refresh_access_token = MagicMock( + name="august.authenticator.refresh_access_token" + ) + return authenticator + + +def _mock_august_authentication(token_text, token_timestamp): + authentication = MagicMock(name="august.authentication") + type(authentication).access_token = PropertyMock(return_value=token_text) + type(authentication).access_token_expires = PropertyMock( + return_value=token_timestamp + ) + return authentication diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py new file mode 100644 index 00000000000..415ce26076a --- /dev/null +++ b/tests/components/august/test_binary_sensor.py @@ -0,0 +1,113 @@ +"""The lock tests for the august platform.""" + +import datetime +from unittest.mock import MagicMock + +from august.activity import ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN +from august.lock import LockDoorStatus + +from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor +from homeassistant.util import dt + +from tests.components.august.mocks import ( + MockActivity, + MockAugustData, + _mock_august_lock, +) + + +class MockAugustDoorBinarySensor(AugustDoorBinarySensor): + """A mock for august component AugustLock class.""" + + def __init__(self, august_data=None): + """Init the mock for august component AugustLock class.""" + self._data = august_data + self._door = _mock_august_lock() + + @property + def name(self): + """Mock name.""" + return "mockedname1" + + @property + def device_id(self): + """Mock device_id.""" + return "mockdeviceid1" + + def _update_door_state(self, door_state, activity_start_time_utc): + """Mock updating the lock status.""" + self._data.set_last_door_state_update_time_utc( + self._door.device_id, activity_start_time_utc + ) + self.last_update_door_state = {} + self.last_update_door_state["door_state"] = door_state + self.last_update_door_state["activity_start_time_utc"] = activity_start_time_utc + return MagicMock() + + +def test__sync_door_activity_doored_via_dooropen(): + """Test _sync_door_activity dooropen.""" + data = MockAugustData(last_door_state_update_timestamp=1) + door = MockAugustDoorBinarySensor(august_data=data) + door_activity_start_timestamp = 1234 + door_activity = MockActivity( + action=ACTION_DOOR_OPEN, + activity_start_timestamp=door_activity_start_timestamp, + activity_end_timestamp=5678, + ) + door._sync_door_activity(door_activity) + assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN + assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(door_activity_start_timestamp) + ) + + +def test__sync_door_activity_doorclosed(): + """Test _sync_door_activity doorclosed.""" + data = MockAugustData(last_door_state_update_timestamp=1) + door = MockAugustDoorBinarySensor(august_data=data) + door_activity_timestamp = 1234 + door_activity = MockActivity( + action=ACTION_DOOR_CLOSED, + activity_start_timestamp=door_activity_timestamp, + activity_end_timestamp=door_activity_timestamp, + ) + door._sync_door_activity(door_activity) + assert door.last_update_door_state["door_state"] == LockDoorStatus.CLOSED + assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(door_activity_timestamp) + ) + + +def test__sync_door_activity_ignores_old_data(): + """Test _sync_door_activity dooropen then expired doorclosed.""" + data = MockAugustData(last_door_state_update_timestamp=1) + door = MockAugustDoorBinarySensor(august_data=data) + first_door_activity_timestamp = 1234 + door_activity = MockActivity( + action=ACTION_DOOR_OPEN, + activity_start_timestamp=first_door_activity_timestamp, + activity_end_timestamp=first_door_activity_timestamp, + ) + door._sync_door_activity(door_activity) + assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN + assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(first_door_activity_timestamp) + ) + + # Now we do the update with an older start time to + # make sure it ignored + data.set_last_door_state_update_time_utc( + door.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000)) + ) + door_activity_timestamp = 2 + door_activity = MockActivity( + action=ACTION_DOOR_CLOSED, + activity_start_timestamp=door_activity_timestamp, + activity_end_timestamp=door_activity_timestamp, + ) + door._sync_door_activity(door_activity) + assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN + assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(first_door_activity_timestamp) + ) diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index e84df35b6b1..97d7d4e613b 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,27 +1,12 @@ """The tests for the august platform.""" -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock from homeassistant.components import august - -def _mock_august_authenticator(): - authenticator = MagicMock(name="august.authenticator") - authenticator.should_refresh = MagicMock( - name="august.authenticator.should_refresh", return_value=0 - ) - authenticator.refresh_access_token = MagicMock( - name="august.authenticator.refresh_access_token" - ) - return authenticator - - -def _mock_august_authentication(token_text, token_timestamp): - authentication = MagicMock(name="august.authentication") - type(authentication).access_token = PropertyMock(return_value=token_text) - type(authentication).access_token_expires = PropertyMock( - return_value=token_timestamp - ) - return authentication +from tests.components.august.mocks import ( + _mock_august_authentication, + _mock_august_authenticator, +) def test__refresh_access_token(): diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py new file mode 100644 index 00000000000..6d33f44e4b7 --- /dev/null +++ b/tests/components/august/test_lock.py @@ -0,0 +1,131 @@ +"""The lock tests for the august platform.""" + +import datetime +from unittest.mock import MagicMock + +from august.activity import ( + ACTION_LOCK_LOCK, + ACTION_LOCK_ONETOUCHLOCK, + ACTION_LOCK_UNLOCK, +) +from august.lock import LockStatus + +from homeassistant.components.august.lock import AugustLock +from homeassistant.util import dt + +from tests.components.august.mocks import ( + MockActivity, + MockAugustData, + _mock_august_lock, +) + + +class MockAugustLock(AugustLock): + """A mock for august component AugustLock class.""" + + def __init__(self, august_data=None): + """Init the mock for august component AugustLock class.""" + self._data = august_data + self._lock = _mock_august_lock() + + @property + def device_id(self): + """Mock device_id.""" + return "mockdeviceid1" + + def _update_lock_status(self, lock_status, activity_start_time_utc): + """Mock updating the lock status.""" + self._data.set_last_lock_status_update_time_utc( + self._lock.device_id, activity_start_time_utc + ) + self.last_update_lock_status = {} + self.last_update_lock_status["lock_status"] = lock_status + self.last_update_lock_status[ + "activity_start_time_utc" + ] = activity_start_time_utc + return MagicMock() + + +def test__sync_lock_activity_locked_via_onetouchlock(): + """Test _sync_lock_activity locking.""" + data = MockAugustData(last_lock_status_update_timestamp=1) + lock = MockAugustLock(august_data=data) + lock_activity_start_timestamp = 1234 + lock_activity = MockActivity( + action=ACTION_LOCK_ONETOUCHLOCK, + activity_start_timestamp=lock_activity_start_timestamp, + activity_end_timestamp=5678, + ) + lock._sync_lock_activity(lock_activity) + assert lock.last_update_lock_status["lock_status"] == LockStatus.LOCKED + assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(lock_activity_start_timestamp) + ) + + +def test__sync_lock_activity_locked_via_lock(): + """Test _sync_lock_activity locking.""" + data = MockAugustData(last_lock_status_update_timestamp=1) + lock = MockAugustLock(august_data=data) + lock_activity_start_timestamp = 1234 + lock_activity = MockActivity( + action=ACTION_LOCK_LOCK, + activity_start_timestamp=lock_activity_start_timestamp, + activity_end_timestamp=5678, + ) + lock._sync_lock_activity(lock_activity) + assert lock.last_update_lock_status["lock_status"] == LockStatus.LOCKED + assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(lock_activity_start_timestamp) + ) + + +def test__sync_lock_activity_unlocked(): + """Test _sync_lock_activity unlocking.""" + data = MockAugustData(last_lock_status_update_timestamp=1) + lock = MockAugustLock(august_data=data) + lock_activity_timestamp = 1234 + lock_activity = MockActivity( + action=ACTION_LOCK_UNLOCK, + activity_start_timestamp=lock_activity_timestamp, + activity_end_timestamp=lock_activity_timestamp, + ) + lock._sync_lock_activity(lock_activity) + assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED + assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(lock_activity_timestamp) + ) + + +def test__sync_lock_activity_ignores_old_data(): + """Test _sync_lock_activity unlocking.""" + data = MockAugustData(last_lock_status_update_timestamp=1) + lock = MockAugustLock(august_data=data) + first_lock_activity_timestamp = 1234 + lock_activity = MockActivity( + action=ACTION_LOCK_UNLOCK, + activity_start_timestamp=first_lock_activity_timestamp, + activity_end_timestamp=first_lock_activity_timestamp, + ) + lock._sync_lock_activity(lock_activity) + assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED + assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(first_lock_activity_timestamp) + ) + + # Now we do the update with an older start time to + # make sure it ignored + data.set_last_lock_status_update_time_utc( + lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000)) + ) + lock_activity_timestamp = 2 + lock_activity = MockActivity( + action=ACTION_LOCK_LOCK, + activity_start_timestamp=lock_activity_timestamp, + activity_end_timestamp=lock_activity_timestamp, + ) + lock._sync_lock_activity(lock_activity) + assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED + assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( + datetime.datetime.fromtimestamp(first_lock_activity_timestamp) + ) From 78783555eac8a750ad564804341f24792fc61b5e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Feb 2020 00:36:04 +0100 Subject: [PATCH 221/378] Fix spelling of VIVOTEK (#31773) * Fix spelling of VIVOTEK in manifest * Also adjust default name of camera to match the brand name --- homeassistant/components/vivotek/camera.py | 4 ++-- homeassistant/components/vivotek/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index f4a195f5b0c..6bf9fdace5a 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -25,8 +25,8 @@ CONF_FRAMERATE = "framerate" CONF_SECURITY_LEVEL = "security_level" CONF_STREAM_PATH = "stream_path" -DEFAULT_CAMERA_BRAND = "Vivotek" -DEFAULT_NAME = "Vivotek Camera" +DEFAULT_CAMERA_BRAND = "VIVOTEK" +DEFAULT_NAME = "VIVOTEK Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 9246bc4c89b..3b4a4211f34 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -1,6 +1,6 @@ { "domain": "vivotek", - "name": "Vivotek", + "name": "VIVOTEK", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": ["libpyvivotek==0.4.0"], "dependencies": [], From aa97be71a81c89d0fb9cf9cebdceb116c41d20b3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Feb 2020 00:36:15 +0100 Subject: [PATCH 222/378] Fix spelling of apcupsd in manifest (#31770) --- homeassistant/components/apcupsd/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index a8a5506fc0a..5908523e6d8 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,6 +1,6 @@ { "domain": "apcupsd", - "name": "APCUPSd", + "name": "apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], "dependencies": [], From be388a479786849438668a3f81d3b0e9d004235e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Feb 2020 01:12:11 +0100 Subject: [PATCH 223/378] Fix spelling of AVM FRITZ!Box in manifest (#31765) --- homeassistant/components/fritz/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 21b86e26af1..5536e8fada3 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,6 +1,6 @@ { "domain": "fritz", - "name": "AVM Fritzbox", + "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": ["fritzconnection==1.2.0"], "dependencies": [], From e0a035ce355ada7846e30e92df3e13b7e7a4e97b Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 13 Feb 2020 01:12:35 +0100 Subject: [PATCH 224/378] Implement PlatformNotReady to Linky + fix TypeError (#31768) --- homeassistant/components/linky/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 9beb9acc403..846b7eeb99f 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ENERGY_KILO_WATT_HOUR, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType @@ -73,6 +74,7 @@ class LinkyAccount: _LOGGER.debug(json.dumps(self._data, indent=2)) except PyLinkyException as exp: _LOGGER.error(exp) + raise PlatformNotReady finally: client.close_session() @@ -146,6 +148,9 @@ class LinkySensor(Entity): async def async_update(self) -> None: """Retrieve the new data for the sensor.""" + if self._account.data is None: + return + data = self._account.data[self._scale][self._when] self._consumption = data[CONSUMPTION] self._time = data[TIME] From 4cac0443e25d7637cf0f3d02d60d1113d7b8f5fc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 13 Feb 2020 01:15:08 +0100 Subject: [PATCH 225/378] UniFi - Change handling of updated options (#31762) * Change handling of updated options * Add tests --- homeassistant/components/unifi/controller.py | 8 +- .../components/unifi/device_tracker.py | 111 ++++++--- homeassistant/components/unifi/sensor.py | 45 ++-- tests/components/unifi/test_device_tracker.py | 230 +++++++++++++++--- tests/components/unifi/test_sensor.py | 24 ++ 5 files changed, 320 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index fc0d1324f47..b7cd8e8b6a1 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -164,7 +164,7 @@ class UniFiController: WIRELESS_GUEST_CONNECTED, ): self.update_wireless_clients() - else: + elif data.get("clients") or data.get("devices"): async_dispatcher_send(self.hass, self.signal_update) @property @@ -238,13 +238,13 @@ class UniFiController: self.api.start_websocket() - self.config_entry.add_update_listener(self.async_options_updated) + self.config_entry.add_update_listener(self.async_config_entry_updated) return True @staticmethod - async def async_options_updated(hass, entry): - """Triggered by config entry options updates.""" + async def async_config_entry_updated(hass, entry) -> None: + """Handle signals of config entry being updated.""" controller_id = CONTROLLER_ID.format( host=entry.data[CONF_CONTROLLER][CONF_HOST], site=entry.data[CONF_CONTROLLER][CONF_SITE_ID], diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 859a37049b0..5dd5f0c83ae 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -6,10 +6,8 @@ from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback -from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER @@ -43,7 +41,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller = get_controller_from_config_entry(hass, config_entry) tracked = {} - registry = await entity_registry.async_get_registry(hass) + option_track_clients = controller.option_track_clients + option_track_devices = controller.option_track_devices + option_track_wired_clients = controller.option_track_wired_clients + + registry = await hass.helpers.entity_registry.async_get_registry() # Restore clients that is not a part of active clients list. for entity in registry.entities.values(): @@ -65,6 +67,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def update_controller(): """Update the values of the controller.""" + nonlocal option_track_clients + nonlocal option_track_devices + + if not option_track_clients and not option_track_devices: + return + add_entities(controller, async_add_entities, tracked) controller.listeners.append( @@ -72,24 +80,59 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) @callback - def update_disable_on_entities(): - """Update the values of the controller.""" - for entity in tracked.values(): + def options_updated(): + """Manage entities affected by config entry options.""" + nonlocal option_track_clients + nonlocal option_track_devices + nonlocal option_track_wired_clients - if entity.entity_registry_enabled_default == entity.enabled: + update = False + remove = set() + + for current_option, config_entry_option, tracker_class in ( + (option_track_clients, controller.option_track_clients, UniFiClientTracker), + (option_track_devices, controller.option_track_devices, UniFiDeviceTracker), + ): + if current_option == config_entry_option: continue - disabled_by = None - if not entity.entity_registry_enabled_default and entity.enabled: - disabled_by = DISABLED_CONFIG_ENTRY + if config_entry_option: + update = True + else: + for mac, entity in tracked.items(): + if isinstance(entity, tracker_class): + remove.add(mac) - registry.async_update_entity( - entity.registry_entry.entity_id, disabled_by=disabled_by - ) + if ( + controller.option_track_clients + and option_track_wired_clients != controller.option_track_wired_clients + ): + + if controller.option_track_wired_clients: + update = True + else: + for mac, entity in tracked.items(): + if isinstance(entity, UniFiClientTracker) and entity.is_wired: + remove.add(mac) + + option_track_clients = controller.option_track_clients + option_track_devices = controller.option_track_devices + option_track_wired_clients = controller.option_track_wired_clients + + for mac in remove: + entity = tracked.pop(mac) + + if registry.async_is_registered(entity.entity_id): + registry.async_remove(entity.entity_id) + + hass.async_create_task(entity.async_remove()) + + if update: + update_controller() controller.listeners.append( async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + hass, controller.signal_options_update, options_updated ) ) @@ -101,16 +144,23 @@ def add_entities(controller, async_add_entities, tracked): """Add new tracker entities from the controller.""" new_tracked = [] - for items, tracker_class in ( - (controller.api.clients, UniFiClientTracker), - (controller.api.devices, UniFiDeviceTracker), + for items, tracker_class, track in ( + (controller.api.clients, UniFiClientTracker, controller.option_track_clients), + (controller.api.devices, UniFiDeviceTracker, controller.option_track_devices), ): + if not track: + continue for item_id in items: if item_id in tracked: continue + if tracker_class is UniFiClientTracker and ( + not controller.option_track_wired_clients and items[item_id].is_wired + ): + continue + tracked[item_id] = tracker_class(items[item_id], controller) new_tracked.append(tracked[item_id]) @@ -130,11 +180,12 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_clients: - return False + def is_connected(self): + """Return true if the client is connected to the network. + If connected to unwanted ssid return False. + If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. + """ if ( not self.is_wired and self.controller.option_ssid_filter @@ -142,17 +193,6 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): ): return False - if not self.controller.option_track_wired_clients and self.is_wired: - return False - - return True - - @property - def is_connected(self): - """Return true if the client is connected to the network. - - If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. - """ if self.is_wired != self.client.is_wired: if not self.wired_bug: self.wired_bug = dt_util.utcnow() @@ -202,13 +242,6 @@ class UniFiDeviceTracker(ScannerEntity): self.controller = controller self.listeners = [] - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - if self.controller.option_track_devices: - return True - return False - async def async_added_to_hass(self): """Subscribe to device events.""" LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 860ddf81d7d..942b0ef6779 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -3,9 +3,7 @@ import logging from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback -from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY from .unifi_client import UniFiClient @@ -24,11 +22,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller = get_controller_from_config_entry(hass, config_entry) sensors = {} - registry = await entity_registry.async_get_registry(hass) + option_allow_bandwidth_sensors = controller.option_allow_bandwidth_sensors + + entity_registry = await hass.helpers.entity_registry.async_get_registry() @callback def update_controller(): """Update the values of the controller.""" + nonlocal option_allow_bandwidth_sensors + + if not option_allow_bandwidth_sensors: + return + add_entities(controller, async_add_entities, sensors) controller.listeners.append( @@ -36,24 +41,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) @callback - def update_disable_on_entities(): + def options_updated(): """Update the values of the controller.""" - for entity in sensors.values(): + nonlocal option_allow_bandwidth_sensors - if entity.entity_registry_enabled_default == entity.enabled: - continue + if option_allow_bandwidth_sensors != controller.option_allow_bandwidth_sensors: + option_allow_bandwidth_sensors = controller.option_allow_bandwidth_sensors - disabled_by = None - if not entity.entity_registry_enabled_default and entity.enabled: - disabled_by = DISABLED_CONFIG_ENTRY + if option_allow_bandwidth_sensors: + update_controller() - registry.async_update_entity( - entity.registry_entry.entity_id, disabled_by=disabled_by - ) + else: + for sensor in sensors.values(): + + if entity_registry.async_is_registered(sensor.entity_id): + entity_registry.async_remove(sensor.entity_id) + + hass.async_create_task(sensor.async_remove()) + + sensors.clear() controller.listeners.append( async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + hass, controller.signal_options_update, options_updated ) ) @@ -87,13 +97,6 @@ def add_entities(controller, async_add_entities, sensors): class UniFiRxBandwidthSensor(UniFiClient): """Receiving bandwidth sensor.""" - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - if self.controller.option_allow_bandwidth_sensors: - return True - return False - @property def state(self): """Return the state of the sensor.""" diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 86595fe43e2..608e72b483a 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -11,6 +11,7 @@ from homeassistant.components import unifi import homeassistant.components.device_tracker as device_tracker from homeassistant.components.unifi.const import ( CONF_SSID_FILTER, + CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, ) @@ -114,7 +115,7 @@ async def test_tracked_devices(hass): devices_response=[DEVICE_1, DEVICE_2], known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -125,7 +126,8 @@ async def test_tracked_devices(hass): assert client_2.state == "not_home" client_3 = hass.states.get("device_tracker.client_3") - assert client_3 is None + assert client_3 is not None + assert client_3.state == "not_home" # Wireless client with wired bug, if bug active on restart mark device away client_4 = hass.states.get("device_tracker.client_4") @@ -136,6 +138,7 @@ async def test_tracked_devices(hass): assert device_1 is not None assert device_1.state == "not_home" + # State change signalling works client_1_copy = copy(CLIENT_1) client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) event = {"meta": {"message": "sta:sync"}, "data": [client_1_copy]} @@ -152,28 +155,6 @@ async def test_tracked_devices(hass): device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" - # Controller unavailable - controller.async_unifi_signalling_callback( - SIGNAL_CONNECTION_STATE, STATE_DISCONNECTED - ) - await hass.async_block_till_done() - - client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == STATE_UNAVAILABLE - - device_1 = hass.states.get("device_tracker.device_1") - assert device_1.state == STATE_UNAVAILABLE - - # Controller available - controller.async_unifi_signalling_callback(SIGNAL_CONNECTION_STATE, STATE_RUNNING) - await hass.async_block_till_done() - - client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "home" - - device_1 = hass.states.get("device_tracker.device_1") - assert device_1.state == "home" - # Disabled device is unavailable device_1_copy = copy(DEVICE_1) device_1_copy["disabled"] = True @@ -184,24 +165,205 @@ async def test_tracked_devices(hass): device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE - # Don't track wired clients nor devices - controller.config_entry.add_update_listener(controller.async_options_updated) - hass.config_entries.async_update_entry( - controller.config_entry, - options={ - CONF_SSID_FILTER: [], - CONF_TRACK_WIRED_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, + +async def test_controller_state_change(hass): + """Verify entities state reflect on controller becoming unavailable.""" + controller = await setup_unifi_integration( + hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1], + ) + assert len(hass.states.async_all()) == 3 + + # Controller unavailable + controller.async_unifi_signalling_callback( + SIGNAL_CONNECTION_STATE, STATE_DISCONNECTED ) await hass.async_block_till_done() + client_1 = hass.states.get("device_tracker.client_1") - assert client_1 + assert client_1.state == STATE_UNAVAILABLE + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == STATE_UNAVAILABLE + + # Controller available + controller.async_unifi_signalling_callback(SIGNAL_CONNECTION_STATE, STATE_RUNNING) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "not_home" + + +async def test_option_track_clients(hass): + """Test the tracking of clients can be turned off.""" + controller = await setup_unifi_integration( + hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], + ) + assert len(hass.states.async_all()) == 4 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_CLIENTS: False}, + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is None + client_2 = hass.states.get("device_tracker.wired_client") assert client_2 is None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_CLIENTS: True}, + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + +async def test_option_track_wired_clients(hass): + """Test the tracking of wired clients can be turned off.""" + controller = await setup_unifi_integration( + hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], + ) + assert len(hass.states.async_all()) == 4 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: False}, + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: True}, + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + +async def test_option_track_devices(hass): + """Test the tracking of devices can be turned off.""" + controller = await setup_unifi_integration( + hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], + ) + assert len(hass.states.async_all()) == 4 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_DEVICES: False}, + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + device_1 = hass.states.get("device_tracker.device_1") assert device_1 is None + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_DEVICES: True}, + ) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + +async def test_option_ssid_filter(hass): + """Test the SSID filter works.""" + controller = await setup_unifi_integration( + hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3], + ) + assert len(hass.states.async_all()) == 2 + + # SSID filter active + client_3 = hass.states.get("device_tracker.client_3") + assert client_3.state == "not_home" + + client_3_copy = copy(CLIENT_3) + client_3_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + event = {"meta": {"message": "sta:sync"}, "data": [client_3_copy]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + # SSID filter active even though time stamp should mark as home + client_3 = hass.states.get("device_tracker.client_3") + assert client_3.state == "not_home" + + # Remove SSID filter + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_SSID_FILTER: []}, + ) + event = {"meta": {"message": "sta:sync"}, "data": [client_3_copy]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + # SSID no longer filtered + client_3 = hass.states.get("device_tracker.client_3") + assert client_3.state == "home" + async def test_wireless_client_go_wired_issue(hass): """Test the solution to catch wireless device go wired UniFi issue. diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 668b7a36ada..c726d3bb1cb 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -99,3 +99,27 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "6789.0" + + hass.config_entries.async_update_entry( + controller.config_entry, + options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: False}, + ) + await hass.async_block_till_done() + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx is None + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx is None + + hass.config_entries.async_update_entry( + controller.config_entry, + options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, + ) + await hass.async_block_till_done() + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "2345.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "6789.0" From 52b16bf5aa20b32cc4a89287444172f8641542d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Feb 2020 16:16:47 -0800 Subject: [PATCH 226/378] Rename codecov so it will be picked up (#31775) --- .codecov.yml => codecov.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .codecov.yml => codecov.yml (100%) diff --git a/.codecov.yml b/codecov.yml similarity index 100% rename from .codecov.yml rename to codecov.yml From 40e866a5bbff5f8fa81a46fce4faac9f5692cac9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 13 Feb 2020 00:31:46 +0000 Subject: [PATCH 227/378] [ci skip] Translation update --- .../abode/.translations/es-419.json | 22 ++++ .../adguard/.translations/es-419.json | 2 + .../airly/.translations/es-419.json | 22 ++++ .../components/airly/.translations/sl.json | 3 + .../components/almond/.translations/sl.json | 4 + .../components/axis/.translations/ca.json | 2 +- .../binary_sensor/.translations/es-419.json | 24 +++- .../brother/.translations/es-419.json | 5 + .../components/brother/.translations/sl.json | 9 ++ .../cert_expiry/.translations/es-419.json | 21 ++++ .../climate/.translations/es-419.json | 10 ++ .../coolmaster/.translations/es-419.json | 13 +++ .../components/demo/.translations/es-419.json | 5 + .../device_tracker/.translations/es-419.json | 8 ++ .../ecobee/.translations/es-419.json | 16 +++ .../elgato/.translations/es-419.json | 17 +++ .../components/fan/.translations/es-419.json | 12 ++ .../garmin_connect/.translations/es-419.json | 22 ++++ .../garmin_connect/.translations/sl.json | 24 ++++ .../components/gdacs/.translations/pl.json | 11 ++ .../components/gdacs/.translations/sl.json | 16 +++ .../components/gios/.translations/es-419.json | 11 ++ .../components/gios/.translations/sl.json | 3 + .../glances/.translations/es-419.json | 30 +++++ .../hangouts/.translations/es-419.json | 1 + .../components/heos/.translations/es-419.json | 4 + .../homekit_controller/.translations/ca.json | 2 +- .../.translations/es-419.json | 13 ++- .../.translations/es-419.json | 3 +- .../konnected/.translations/ca.json | 93 +++++++++++++++ .../konnected/.translations/ko.json | 66 +++++++++++ .../konnected/.translations/nl.json | 26 +++++ .../konnected/.translations/pl.json | 29 +++++ .../konnected/.translations/ru.json | 66 +++++++++++ .../konnected/.translations/sl.json | 106 ++++++++++++++++++ .../konnected/.translations/sv.json | 64 +++++++++++ .../components/linky/.translations/sl.json | 3 + .../components/melcloud/.translations/ko.json | 23 ++++ .../components/melcloud/.translations/pl.json | 9 ++ .../components/melcloud/.translations/sl.json | 23 ++++ .../components/melcloud/.translations/sv.json | 23 ++++ .../meteo_france/.translations/sl.json | 18 +++ .../components/mikrotik/.translations/sl.json | 37 ++++++ .../minecraft_server/.translations/pl.json | 21 ++++ .../minecraft_server/.translations/sl.json | 24 ++++ .../minecraft_server/.translations/sv.json | 24 ++++ .../components/netatmo/.translations/sl.json | 18 +++ .../components/ring/.translations/sl.json | 27 +++++ .../samsungtv/.translations/sl.json | 28 +++++ .../smartthings/.translations/sv.json | 2 +- .../components/spotify/.translations/sl.json | 18 +++ .../components/vilfo/.translations/en.json | 23 ++++ .../components/vilfo/.translations/sv.json | 23 ++++ .../components/vizio/.translations/ca.json | 2 +- .../components/vizio/.translations/sl.json | 44 ++++++++ .../components/withings/.translations/sl.json | 5 + 56 files changed, 1172 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/abode/.translations/es-419.json create mode 100644 homeassistant/components/airly/.translations/es-419.json create mode 100644 homeassistant/components/brother/.translations/es-419.json create mode 100644 homeassistant/components/climate/.translations/es-419.json create mode 100644 homeassistant/components/coolmaster/.translations/es-419.json create mode 100644 homeassistant/components/demo/.translations/es-419.json create mode 100644 homeassistant/components/device_tracker/.translations/es-419.json create mode 100644 homeassistant/components/ecobee/.translations/es-419.json create mode 100644 homeassistant/components/elgato/.translations/es-419.json create mode 100644 homeassistant/components/fan/.translations/es-419.json create mode 100644 homeassistant/components/garmin_connect/.translations/es-419.json create mode 100644 homeassistant/components/garmin_connect/.translations/sl.json create mode 100644 homeassistant/components/gdacs/.translations/pl.json create mode 100644 homeassistant/components/gdacs/.translations/sl.json create mode 100644 homeassistant/components/gios/.translations/es-419.json create mode 100644 homeassistant/components/glances/.translations/es-419.json create mode 100644 homeassistant/components/konnected/.translations/ca.json create mode 100644 homeassistant/components/konnected/.translations/ko.json create mode 100644 homeassistant/components/konnected/.translations/nl.json create mode 100644 homeassistant/components/konnected/.translations/pl.json create mode 100644 homeassistant/components/konnected/.translations/ru.json create mode 100644 homeassistant/components/konnected/.translations/sl.json create mode 100644 homeassistant/components/konnected/.translations/sv.json create mode 100644 homeassistant/components/melcloud/.translations/ko.json create mode 100644 homeassistant/components/melcloud/.translations/pl.json create mode 100644 homeassistant/components/melcloud/.translations/sl.json create mode 100644 homeassistant/components/melcloud/.translations/sv.json create mode 100644 homeassistant/components/meteo_france/.translations/sl.json create mode 100644 homeassistant/components/mikrotik/.translations/sl.json create mode 100644 homeassistant/components/minecraft_server/.translations/pl.json create mode 100644 homeassistant/components/minecraft_server/.translations/sl.json create mode 100644 homeassistant/components/minecraft_server/.translations/sv.json create mode 100644 homeassistant/components/netatmo/.translations/sl.json create mode 100644 homeassistant/components/ring/.translations/sl.json create mode 100644 homeassistant/components/samsungtv/.translations/sl.json create mode 100644 homeassistant/components/spotify/.translations/sl.json create mode 100644 homeassistant/components/vilfo/.translations/en.json create mode 100644 homeassistant/components/vilfo/.translations/sv.json create mode 100644 homeassistant/components/vizio/.translations/sl.json diff --git a/homeassistant/components/abode/.translations/es-419.json b/homeassistant/components/abode/.translations/es-419.json new file mode 100644 index 00000000000..f2def50d063 --- /dev/null +++ b/homeassistant/components/abode/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." + }, + "error": { + "connection_error": "No se puede conectar a Abode.", + "identifier_exists": "Cuenta ya registrada.", + "invalid_credentials": "Credenciales inv\u00e1lidas." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/es-419.json b/homeassistant/components/adguard/.translations/es-419.json index ed8e0c3a358..eb3274f19b6 100644 --- a/homeassistant/components/adguard/.translations/es-419.json +++ b/homeassistant/components/adguard/.translations/es-419.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}. Actualice su complemento Hass.io AdGuard Home.", + "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}.", "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.", "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/airly/.translations/es-419.json b/homeassistant/components/airly/.translations/es-419.json new file mode 100644 index 00000000000..74924493863 --- /dev/null +++ b/homeassistant/components/airly/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/sl.json b/homeassistant/components/airly/.translations/sl.json index 08f57d88bcb..f8ca4e5b6d5 100644 --- a/homeassistant/components/airly/.translations/sl.json +++ b/homeassistant/components/airly/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly integracija za te koordinate je \u017ee nastavljen." + }, "error": { "auth": "Klju\u010d API ni pravilen.", "name_exists": "Ime \u017ee obstaja", diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json index 086190590ac..4a593cc5605 100644 --- a/homeassistant/components/almond/.translations/sl.json +++ b/homeassistant/components/almond/.translations/sl.json @@ -6,6 +6,10 @@ "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." }, "step": { + "hassio_confirm": { + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Hass.io: {addon} ?", + "title": "Almond prek dodatka Hass.io" + }, "pick_implementation": { "title": "Izberite na\u010din preverjanja pristnosti" } diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index ecf7b552bba..58f5c0e4ad2 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -9,7 +9,7 @@ }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.", "device_unavailable": "El dispositiu no est\u00e0 disponible", "faulty_credentials": "Credencials d'usuari incorrectes" }, diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json index f1c20e5346b..e727e18775a 100644 --- a/homeassistant/components/binary_sensor/.translations/es-419.json +++ b/homeassistant/components/binary_sensor/.translations/es-419.json @@ -28,6 +28,12 @@ "is_not_occupied": "{entity_name} no est\u00e1 ocupado", "is_not_open": "{entity_name} est\u00e1 cerrado", "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 enchufado", "is_powered": "{entity_name} est\u00e1 encendido", "is_present": "{entity_name} est\u00e1 presente", "is_problem": "{entity_name} est\u00e1 detectando un problema", @@ -45,6 +51,7 @@ "hot": "{entity_name} se calent\u00f3", "light": "{entity_name} comenz\u00f3 a detectar luz", "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedeci\u00f3", "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} comenz\u00f3 a moverse", @@ -59,7 +66,22 @@ "not_cold": "{entity_name} no se enfri\u00f3", "not_connected": "{entity_name} desconectado", "not_hot": "{entity_name} no se calent\u00f3", - "not_locked": "{entity_name} desbloqueado" + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_opened": "{entity_name} cerrado", + "not_plugged_in": "{entity_name} desconectado", + "not_present": "{entity_name} no presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se ocup\u00f3", + "opened": "{entity_name} abierto", + "plugged_in": "{entity_name} enchufado", + "present": "{entity_name} presente", + "problem": "{entity_name} comenz\u00f3 a detectar problemas", + "smoke": "{entity_name} comenz\u00f3 a detectar humo", + "sound": "{entity_name} comenz\u00f3 a detectar sonido", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } } } \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/es-419.json b/homeassistant/components/brother/.translations/es-419.json new file mode 100644 index 00000000000..49b77b829b5 --- /dev/null +++ b/homeassistant/components/brother/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Impresora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/sl.json b/homeassistant/components/brother/.translations/sl.json index 99caf69a86f..d22f128ffbe 100644 --- a/homeassistant/components/brother/.translations/sl.json +++ b/homeassistant/components/brother/.translations/sl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ta tiskalnik je \u017ee konfiguriran.", "unsupported_model": "Ta model tiskalnika ni podprt." }, "error": { @@ -8,6 +9,7 @@ "snmp_error": "Stre\u017enik SNMP je izklopljen ali tiskalnik ni podprt.", "wrong_host": "Neveljavno ime gostitelja ali IP naslov." }, + "flow_title": "Tiskalnik Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -16,6 +18,13 @@ }, "description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother", "title": "Brother Tiskalnik" + }, + "zeroconf_confirm": { + "data": { + "type": "Vrsta tiskalnika" + }, + "description": "Ali \u017eelite dodati Brother tiskalnik {model} s serijsko \u0161tevilko ' {serial_number} ' v Home Assistant?", + "title": "Odkriti Brother tiskalniki" } }, "title": "Brother Tiskalnik" diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json index 392dbf35f5a..e350faffcb3 100644 --- a/homeassistant/components/cert_expiry/.translations/es-419.json +++ b/homeassistant/components/cert_expiry/.translations/es-419.json @@ -1,5 +1,26 @@ { "config": { + "abort": { + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + }, + "error": { + "certificate_error": "El certificado no pudo ser validado", + "certificate_fetch_failed": "No se puede recuperar el certificado de esta combinaci\u00f3n de host y puerto", + "connection_timeout": "Tiempo de espera al conectarse a este host", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", + "resolve_failed": "Este host no puede resolverse", + "wrong_host": "El certificado no coincide con el nombre de host" + }, + "step": { + "user": { + "data": { + "host": "El nombre de host del certificado", + "name": "El nombre del certificado", + "port": "El puerto del certificado" + }, + "title": "Definir el certificado para probar" + } + }, "title": "Expiraci\u00f3n del certificado" } } \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/es-419.json b/homeassistant/components/climate/.translations/es-419.json new file mode 100644 index 00000000000..f3b861b9195 --- /dev/null +++ b/homeassistant/components/climate/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/es-419.json b/homeassistant/components/coolmaster/.translations/es-419.json new file mode 100644 index 00000000000..2bcdecb2aec --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "off": "Puede ser apagado" + }, + "title": "Configure los detalles de su conexi\u00f3n CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/es-419.json b/homeassistant/components/demo/.translations/es-419.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/es-419.json b/homeassistant/components/device_tracker/.translations/es-419.json new file mode 100644 index 00000000000..cfbf7bcfe3e --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/es-419.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} est\u00e1 en casa", + "is_not_home": "{entity_name} no est\u00e1 en casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/es-419.json b/homeassistant/components/ecobee/.translations/es-419.json new file mode 100644 index 00000000000..3e19977f10f --- /dev/null +++ b/homeassistant/components/ecobee/.translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee." + }, + "error": { + "pin_request_failed": "Error al solicitar PIN de ecobee; verifique que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntelo de nuevo." + }, + "step": { + "authorize": { + "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/es-419.json b/homeassistant/components/elgato/.translations/es-419.json new file mode 100644 index 00000000000..2653060030a --- /dev/null +++ b/homeassistant/components/elgato/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "port": "N\u00famero de puerto" + }, + "description": "Configure su Elgato Key Light para integrarse con Home Assistant." + }, + "zeroconf_confirm": { + "title": "Dispositivo Elgato Key Light descubierto" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/es-419.json b/homeassistant/components/fan/.translations/es-419.json new file mode 100644 index 00000000000..dd0c006d760 --- /dev/null +++ b/homeassistant/components/fan/.translations/es-419.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido" + }, + "trigger_type": { + "turned_off": "{entity_name} se apag\u00f3", + "turned_on": "{entity_name} se encendi\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/es-419.json b/homeassistant/components/garmin_connect/.translations/es-419.json new file mode 100644 index 00000000000..6e20b4cd2cc --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cuenta ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente.", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.", + "unknown": "Error inesperado." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Ingrese sus credenciales." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/sl.json b/homeassistant/components/garmin_connect/.translations/sl.json new file mode 100644 index 00000000000..5b85611d5b7 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ta ra\u010dun je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova.", + "invalid_auth": "Neveljavna avtentikacija.", + "too_many_requests": "Preve\u010d zahtev, poskusite pozneje.", + "unknown": "Nepri\u010dakovana napaka." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite svoje poverilnice.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/pl.json b/homeassistant/components/gdacs/.translations/pl.json new file mode 100644 index 00000000000..3932e3398bc --- /dev/null +++ b/homeassistant/components/gdacs/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Promie\u0144" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/sl.json b/homeassistant/components/gdacs/.translations/sl.json new file mode 100644 index 00000000000..fc522a1a263 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Lokacija je \u017ee nastavljena." + }, + "step": { + "user": { + "data": { + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "Globalni sistem opozarjanja in koordinacije nesre\u010d (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/es-419.json b/homeassistant/components/gios/.translations/es-419.json new file mode 100644 index 00000000000..53439a7ab7b --- /dev/null +++ b/homeassistant/components/gios/.translations/es-419.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nombre de la integraci\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/sl.json b/homeassistant/components/gios/.translations/sl.json index da3995dd0b3..089435dee3f 100644 --- a/homeassistant/components/gios/.translations/sl.json +++ b/homeassistant/components/gios/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a integracija za to merilno postajo je \u017ee nastavljena." + }, "error": { "cannot_connect": "Ne morem se povezati s stre\u017enikom GIO\u015a.", "invalid_sensors_data": "Neveljavni podatki senzorjev za to merilno postajo.", diff --git a/homeassistant/components/glances/.translations/es-419.json b/homeassistant/components/glances/.translations/es-419.json new file mode 100644 index 00000000000..6debc6da6c1 --- /dev/null +++ b/homeassistant/components/glances/.translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_version": "Versi\u00f3n no compatible (2 o 3 solamente)" + }, + "step": { + "user": { + "data": { + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario", + "verify_ssl": "Verificar la certificaci\u00f3n del sistema", + "version": "Versi\u00f3n de API de Glances (2 o 3)" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json index 3a297eb15ea..011060694a7 100644 --- a/homeassistant/components/hangouts/.translations/es-419.json +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -5,6 +5,7 @@ "unknown": "Se produjo un error desconocido." }, "error": { + "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, intente nuevamente.", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 4d442a4543b..b0d1d7dc3fb 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -3,8 +3,12 @@ "abort": { "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." }, + "error": { + "connection_failure": "No se puede conectar con el host especificado." + }, "step": { "user": { + "description": "Ingrese el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", "title": "Con\u00e9ctate a Heos" } }, diff --git a/homeassistant/components/homekit_controller/.translations/ca.json b/homeassistant/components/homekit_controller/.translations/ca.json index f2ed4bd0c21..1d2331870e1 100644 --- a/homeassistant/components/homekit_controller/.translations/ca.json +++ b/homeassistant/components/homekit_controller/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "No s'ha pogut vincular, no s'ha trobat el dispositiu.", "already_configured": "Accessori ja configurat amb aquest controlador.", - "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.", "already_paired": "Aquest accessori ja est\u00e0 vinculat amb un altre dispositiu. Reinicia l'accessori i torna-ho a provar.", "ignored_model": "La disponibilitat de HomeKit per aquest model est\u00e0 bloquejada ja que, de moment, no hi ha una integraci\u00f3 nativa completa.", "invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2, hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.", diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json index 67a65f752b4..a99011cf8b1 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -4,17 +4,26 @@ "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo." }, + "error": { + "pairing_failed": "Se produjo un error no controlado al intentar vincularse con este dispositivo. Esto puede ser una falla temporal o su dispositivo puede no ser compatible actualmente.", + "unable_to_pair": "No se puede vincular, por favor intente nuevamente.", + "unknown_error": "El dispositivo inform\u00f3 un error desconocido. Vinculaci\u00f3n fallida." + }, "flow_title": "Accesorio HomeKit: {name}", "step": { "pair": { "data": { "pairing_code": "C\u00f3digo de emparejamiento" - } + }, + "description": "Ingrese su c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio", + "title": "Vincular con el accesorio HomeKit" }, "user": { "data": { "device": "Dispositivo" - } + }, + "description": "Seleccione el dispositivo con el que desea vincular", + "title": "Vincular con el accesorio HomeKit" } }, "title": "Accesorio HomeKit" diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index 5102b25aaee..0919e211617 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -16,7 +16,8 @@ "hapid": "ID de punto de acceso (SGTIN)", "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", "pin": "C\u00f3digo PIN (opcional)" - } + }, + "title": "Elija el punto de acceso HomematicIP" }, "link": { "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)" diff --git a/homeassistant/components/konnected/.translations/ca.json b/homeassistant/components/konnected/.translations/ca.json new file mode 100644 index 00000000000..fb5f86c4ff6 --- /dev/null +++ b/homeassistant/components/konnected/.translations/ca.json @@ -0,0 +1,93 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.", + "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io", + "unknown": "S'ha produ\u00eft un error desconegut" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb el panell Konnected a {host}:{port}" + }, + "step": { + "confirm": { + "description": "Model: {model} \nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d\u2019alarma Konnected.", + "title": "Dispositiu Konnected llest" + }, + "user": { + "data": { + "host": "Adre\u00e7a IP del dispositiu Konnected", + "port": "Port del dispositiu Konnected" + } + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverteix l'estat obert/tancat", + "name": "Nom (opcional)", + "type": "Tipus de sensor binari" + }, + "description": "Selecciona les opcions pel sensor binari de {zone}", + "title": "Configuraci\u00f3 de sensor binari" + }, + "options_digital": { + "data": { + "name": "Nom (opcional)", + "poll_interval": "Interval de sondeig (minuts) (opcional)", + "type": "Tipus de sensor" + }, + "description": "Selecciona les opcions pel sensor digital de {zone}", + "title": "Configuraci\u00f3 de sensor digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "OUT (sortida)" + }, + "title": "Configuraci\u00f3 E/S (I/O)" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARMA1", + "alarm2_out2": "OUT2/ALARMA2", + "out1": "OUT1" + } + }, + "options_misc": { + "data": { + "blink": "Parpelleja el LED del panell quan s'envien canvis d'estat" + }, + "description": "Selecciona el comportament desitjat del panell" + }, + "options_switch": { + "data": { + "momentary": "Durada del pols (ms) (opcional)", + "name": "Nom (opcional)", + "pause": "Pausa entre polsos (ms) (opcional)", + "repeat": "Repeticions (-1 = infinit) (opcional)" + }, + "description": "Selecciona les opcions de sortida per a {zone}", + "title": "Configuraci\u00f3 de sortida commutable" + } + }, + "title": "Opcions del panell d'alarma Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ko.json b/homeassistant/components/konnected/.translations/ko.json new file mode 100644 index 00000000000..29e7fd10c5f --- /dev/null +++ b/homeassistant/components/konnected/.translations/ko.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "{host}:{port} \uc758 Konnected \ud328\ub110\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\ubaa8\ub378: {model}\n\ud638\uc2a4\ud2b8: {host}\n\ud3ec\ud2b8: {port}\n\nKonnected \uc54c\ub78c \ud328\ub110 \uc124\uc815\uc5d0\uc11c IO \uc640 \ud328\ub110 \ub3d9\uc791\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Konnected \uae30\uae30 \uc900\ube44" + }, + "user": { + "data": { + "host": "Konnected \uae30\uae30 IP \uc8fc\uc18c", + "port": "Konnected \uae30\uae30 \ud3ec\ud2b8" + }, + "description": "Konnected \ud328\ub110\uc758 \ud638\uc2a4\ud2b8 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Konnected \uae30\uae30 \ucc3e\uae30" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + }, + "step": { + "options_binary": { + "data": { + "inverse": "\uc5f4\ub9bc / \ub2eb\ud798 \uc0c1\ud0dc \ubc18\uc804", + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615" + }, + "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \uc774\uc9c4 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\uc774\uc9c4 \uc13c\uc11c \uad6c\uc131" + }, + "options_digital": { + "data": { + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "poll_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ubd84) (\uc120\ud0dd \uc0ac\ud56d)", + "type": "\uc13c\uc11c \uc720\ud615" + }, + "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \ub514\uc9c0\ud138 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\ub514\uc9c0\ud138 \uc13c\uc11c \uad6c\uc131" + }, + "options_io": { + "data": { + "1": "\uad6c\uc5ed 1", + "2": "\uad6c\uc5ed 2", + "3": "\uad6c\uc5ed 3", + "4": "\uad6c\uc5ed 4", + "5": "\uad6c\uc5ed 5", + "6": "\uad6c\uc5ed 6", + "7": "\uad6c\uc5ed 7", + "out": "\uc678\ubd80" + }, + "description": "{host} \uc5d0\uc11c {model} \uc744(\ub97c) \ubc1c\uacac\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc9c4 \uc13c\uc11c(\uac1c\ud3d0 \uc811\uc810), \ub514\uc9c0\ud138 \uc13c\uc11c(dht \ubc0f ds18b20) \ub610\ub294 \uc2a4\uc704\uce58\uac00 \uac00\ub2a5\ud55c \uc13c\uc11c\uc758 I/O \uc5d0 \ub530\ub77c \uc544\ub798\uc5d0\uc11c \uac01 I/O \uc758 \uae30\ubcf8 \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/nl.json b/homeassistant/components/konnected/.translations/nl.json new file mode 100644 index 00000000000..dd8c151881c --- /dev/null +++ b/homeassistant/components/konnected/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "options": { + "step": { + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7" + } + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/pl.json b/homeassistant/components/konnected/.translations/pl.json new file mode 100644 index 00000000000..398a75ae58e --- /dev/null +++ b/homeassistant/components/konnected/.translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" + }, + "title": "Konnected.io" + }, + "options": { + "step": { + "options_binary": { + "data": { + "name": "Nazwa (opcjonalnie)" + } + }, + "options_digital": { + "data": { + "name": "Nazwa (opcjonalnie)", + "type": "Typ czujnika" + } + }, + "options_switch": { + "data": { + "name": "Nazwa (opcjonalnie)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json new file mode 100644 index 00000000000..e8cae967b50 --- /dev/null +++ b/homeassistant/components/konnected/.translations/ru.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0430\u043d\u0435\u043b\u0438 Konnected {host}:{port}." + }, + "step": { + "confirm": { + "description": "\u041c\u043e\u0434\u0435\u043b\u044c: {model}\n\u0425\u043e\u0441\u0442: {host}\n\u041f\u043e\u0440\u0442: {port}\n\n\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043b\u043e\u0433\u0438\u043a\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0430\u043d\u0435\u043b\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0445\u043e\u0434\u043e\u0432 \u0438 \u0432\u044b\u0445\u043e\u0434\u043e\u0432 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u0430\u043d\u0435\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Konnected.", + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected \u0433\u043e\u0442\u043e\u0432\u043e \u043a \u0440\u0430\u0431\u043e\u0442\u0435." + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u043f\u0430\u043d\u0435\u043b\u0438 Konnected.", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e." + }, + "step": { + "options_binary": { + "data": { + "inverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0435/\u0437\u0430\u043a\u0440\u044b\u0442\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "type": "\u0422\u0438\u043f \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "options_digital": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "poll_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "type": "\u0422\u0438\u043f \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u0430\u0442\u0447\u0438\u043a\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "options_io": { + "data": { + "1": "\u0417\u043e\u043d\u0430 1", + "2": "\u0417\u043e\u043d\u0430 2", + "3": "\u0417\u043e\u043d\u0430 3", + "4": "\u0417\u043e\u043d\u0430 4", + "5": "\u0417\u043e\u043d\u0430 5", + "6": "\u0417\u043e\u043d\u0430 6", + "7": "\u0417\u043e\u043d\u0430 7", + "out": "\u0412\u042b\u0425\u041e\u0414" + }, + "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {model} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {host}. \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432, \u043a \u043f\u0430\u043d\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 (dht \u0438 ds18b20) \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u044b\u0435 \u0432\u044b\u0445\u043e\u0434\u044b. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/sl.json b/homeassistant/components/konnected/.translations/sl.json new file mode 100644 index 00000000000..38396d0832d --- /dev/null +++ b/homeassistant/components/konnected/.translations/sl.json @@ -0,0 +1,106 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "already_in_progress": "Konfiguracijski tok za napravo je \u017ee v teku.", + "not_konn_panel": "Ni prepoznana kot Konnected.io naprava", + "unknown": "Pri\u0161lo je do neznane napake" + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s Konnected plo\u0161\u010do v {Host}: {Port}" + }, + "step": { + "confirm": { + "description": "Model: {model}\nGostitelj: {host}\nVrata: {port}\n\nV nastavitvah lahko nastavite vedenje I / O in plo\u0161\u010de Konnected alarma. ", + "title": "Konnected naprava pripravljena" + }, + "user": { + "data": { + "host": "IP-naslov Konnected naprave", + "port": "Vrata Konnected naprave" + }, + "description": "Vnesite podatke o gostitelju v svoj Konnected Panel.", + "title": "Odkrijte Konnected napravo" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Ni prepoznana kot Konnected.io naprava" + }, + "error": { + "few": "nekaj", + "one": "ena", + "other": "drugo", + "two": "dva" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Zamenjajte odprto / zaprto stanje", + "name": "Ime (neobvezno)", + "type": "Vrsta binarnega senzorja" + }, + "description": "Izberite mo\u017enosti za binarni senzor, priklju\u010den na {zone}", + "title": "Konfigurirajte binarni senzor" + }, + "options_digital": { + "data": { + "name": "Ime (neobvezno)", + "poll_interval": "Interval osve\u017eevanja (minute) (neobvezno)", + "type": "Vrsta tipala" + }, + "description": "Izberite mo\u017enosti za digitalni senzor, priklju\u010den na {zone}", + "title": "Konfigurirajte digitalni senzor" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + }, + "description": "Odkrili {model} na {host} . Spodaj izberite osnovno konfiguracijo vsakega I / O - odvisno od I / O lahko omogo\u010da binarne senzorje (odpiranje / zapiranje kontaktov), digitalne senzorje (dht in ds18b20) ali preklopne izhode. Podrobne mo\u017enosti boste lahko konfigurirali v naslednjih korakih.", + "title": "Konfigurirajte I / O" + }, + "options_io_ext": { + "data": { + "10": "Obmo\u010dje 10", + "11": "Obmo\u010dje 11", + "12": "Obmo\u010dje 12", + "8": "Obmo\u010dje 8", + "9": "Obmo\u010dje 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2 / ALARM2", + "out1": "OUT1" + }, + "description": "Izberite konfiguracijo preostalega I/O spodaj. Podrobne mo\u017enosti boste lahko konfigurirali v naslednjih korakih.", + "title": "Konfigurirajte raz\u0161irjeni I/O" + }, + "options_misc": { + "data": { + "blink": "Lu\u010dka LED na zaslonu utripa, ko po\u0161iljate spremembo stanja" + }, + "description": "Izberite \u017eeleno vedenje za va\u0161o plo\u0161\u010do", + "title": "Konfigurirajte Razno" + }, + "options_switch": { + "data": { + "activation": "Izhod, ko je vklopljen", + "momentary": "Trajanje impulza (ms) (neobvezno)", + "name": "Ime (neobvezno)", + "pause": "Premor med impulzi (ms) (neobvezno)", + "repeat": "\u010casi ponovitve (-1 = neskon\u010dno) (neobvezno)" + }, + "description": "Izberite izhodne mo\u017enosti za {zone}", + "title": "Konfigurirajte preklopni izhod" + } + }, + "title": "Mo\u017enosti Konnected alarm-a" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/sv.json b/homeassistant/components/konnected/.translations/sv.json new file mode 100644 index 00000000000..f6702745542 --- /dev/null +++ b/homeassistant/components/konnected/.translations/sv.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.", + "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet", + "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till en ansluten panel p\u00e5 {host}:{port}" + }, + "step": { + "confirm": { + "title": "Konnected-enheten redo" + }, + "user": { + "data": { + "host": "Konnected-enhetens IP-adress", + "port": "Konnected-enhetens port" + }, + "description": "Ange v\u00e4rdinformationen f\u00f6r din Konnected Panel.", + "title": "Uppt\u00e4ck Konnected-enhet" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertera \u00f6ppet/st\u00e4ngt tillst\u00e5nd", + "name": "Namn (valfritt)", + "type": "Bin\u00e4r sensortyp" + }, + "title": "Konfigurera Bin\u00e4r Sensor" + }, + "options_digital": { + "data": { + "name": "Namn (valfritt)", + "poll_interval": "H\u00e4mtningsintervall (minuter) (valfritt)", + "type": "Sensortyp" + } + }, + "options_io_ext": { + "title": "Konfigurera ut\u00f6kat I/O" + }, + "options_misc": { + "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", + "title": "Konfigurera \u00d6vrigt" + }, + "options_switch": { + "data": { + "name": "Namn (valfritt)", + "pause": "Paus mellan pulser (ms) (valfritt)" + }, + "title": "Konfigurera v\u00e4xelbar utdata" + } + }, + "title": "Alternativ f\u00f6r Konnected alarmpanel" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json index ab5e054db1e..6ebe598e882 100644 --- a/homeassistant/components/linky/.translations/sl.json +++ b/homeassistant/components/linky/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ra\u010dun \u017ee nastavljen" + }, "error": { "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo", "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)", diff --git a/homeassistant/components/melcloud/.translations/ko.json b/homeassistant/components/melcloud/.translations/ko.json new file mode 100644 index 00000000000..1557abf5a32 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \uc774\uba54\uc77c\uc5d0 \ub300\ud55c MELCloud \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uac31\uc2e0\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "MELCloud \uc758 \ube44\ubc00\ubc88\ud638\ub97c \ub123\uc5b4\uc8fc\uc138\uc694.", + "username": "MELCloud \ub85c\uadf8\uc778 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \ub123\uc5b4\uc8fc\uc138\uc694." + }, + "description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.", + "title": "MELCloud \uc5f0\uacb0" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/pl.json b/homeassistant/components/melcloud/.translations/pl.json new file mode 100644 index 00000000000..c374e7d3e2a --- /dev/null +++ b/homeassistant/components/melcloud/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Niespodziewany b\u0142\u0105d" + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/sl.json b/homeassistant/components/melcloud/.translations/sl.json new file mode 100644 index 00000000000..04dbb953d0d --- /dev/null +++ b/homeassistant/components/melcloud/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za to e-po\u0161to je \u017ee konfigurirana integracija MELCloud. \u017deton za dostop je bil osve\u017een." + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "MELCloud geslo.", + "username": "E-po\u0161tni naslov za prijavo v MELCloud." + }, + "description": "Pove\u017eite se s svojim ra\u010dunom MELCloud.", + "title": "Pove\u017eite se z MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/sv.json b/homeassistant/components/melcloud/.translations/sv.json new file mode 100644 index 00000000000..72a251ef9d0 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud-integration redan konfigurerad f\u00f6r den h\u00e4r e-postadressen. \u00c5tkomsttoken har uppdaterats." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "MELCloud-l\u00f6senord.", + "username": "E-post som anv\u00e4nds f\u00f6r att logga in p\u00e5 MELCloud." + }, + "description": "Anslut med ditt MELCloud-konto.", + "title": "Anslut till MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/sl.json b/homeassistant/components/meteo_france/.translations/sl.json new file mode 100644 index 00000000000..845a89c4775 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Mesto je \u017ee konfigurirano", + "unknown": "Neznana napaka: poskusite pozneje" + }, + "step": { + "user": { + "data": { + "city": "Mesto" + }, + "description": "Vnesite po\u0161tno \u0161tevilko (samo za Francijo) ali ime mesta", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/sl.json b/homeassistant/components/mikrotik/.translations/sl.json new file mode 100644 index 00000000000..a10508f8bbe --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/sl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik je \u017ee konfiguriran" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "name_exists": "Ime obstaja", + "wrong_credentials": "Napa\u010dne poverilnice" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "Uporaba SSL" + }, + "title": "Nastavite Mikrotik usmerjevalnik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Omogo\u010di ARP ping", + "detection_time": "Interval \"doma\" ", + "force_dhcp": "Vsilite skeniranje z uporabo DHCP-ja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/pl.json b/homeassistant/components/minecraft_server/.translations/pl.json new file mode 100644 index 00000000000..94f9fd20af3 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany." + }, + "error": { + "invalid_port": "Port musi znajdowa\u0107 si\u0119 w zakresie od 1024 do 65535. Popraw go i spr\u00f3buj ponownie." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "port": "Port" + }, + "title": "Po\u0142\u0105cz sw\u00f3j serwer Minecraft" + } + }, + "title": "Serwer Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/sl.json b/homeassistant/components/minecraft_server/.translations/sl.json new file mode 100644 index 00000000000..cf8a8af54ee --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Povezava s stre\u017enikom ni uspela. Preverite gostitelja in vrata in poskusite znova. Zagotovite tudi, da na stre\u017eniku izvajate vsaj Minecraft razli\u010dice 1.7.", + "invalid_ip": "IP naslov ni veljaven (MAC naslova ni mogo\u010de dolo\u010diti). Popravite ga in poskusite znova.", + "invalid_port": "Vrata morajo biti v razponu od 1024 do 65535. Prosimo, popravite in poskusite znova." + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "port": "Vrata" + }, + "description": "Nastavite svoj Minecraft stre\u017enik, da omogo\u010dite spremljanje.", + "title": "Pove\u017eite svoj Minecraft stre\u017enik" + } + }, + "title": "Minecraft stre\u017enik" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/sv.json b/homeassistant/components/minecraft_server/.translations/sv.json new file mode 100644 index 00000000000..acf941878dd --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Misslyckades med att ansluta till servern. Kontrollera v\u00e4rden och porten och f\u00f6rs\u00f6k igen. Se ocks\u00e5 till att du k\u00f6r minst Minecraft version 1.7 p\u00e5 din server.", + "invalid_ip": "IP-adressen \u00e4r ogiltig (MAC-adressen kunde inte fastst\u00e4llas). Korrigera det och f\u00f6rs\u00f6k igen.", + "invalid_port": "Porten m\u00e5ste ligga inom intervallet 1024 till 65535. Korrigera den och f\u00f6rs\u00f6k igen." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "port": "Port" + }, + "description": "St\u00e4ll in din Minecraft Server-instans f\u00f6r att till\u00e5ta \u00f6vervakning.", + "title": "L\u00e4nka din Minecraft-server" + } + }, + "title": "Minecraft-server" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/sl.json b/homeassistant/components/netatmo/.translations/sl.json new file mode 100644 index 00000000000..5288c84e44b --- /dev/null +++ b/homeassistant/components/netatmo/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo en ra\u010dun Netatmo.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Komponenta Netatmo ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjeno z Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/sl.json b/homeassistant/components/ring/.translations/sl.json new file mode 100644 index 00000000000..58e86634312 --- /dev/null +++ b/homeassistant/components/ring/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "2fa": { + "data": { + "2fa": "Dvofaktorska koda" + }, + "title": "Dvofaktorska avtentikacija" + }, + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "title": "Prijava s ra\u010dunom Ring" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/sl.json b/homeassistant/components/samsungtv/.translations/sl.json new file mode 100644 index 00000000000..95286476ed0 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/sl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ta televizor Samsung je \u017ee konfiguriran.", + "already_in_progress": "Konfiguracija Samsung TV je \u017ee v teku.", + "auth_missing": "Home Assistant nima dovoljenja za povezavo s tem televizorjem Samsung. Preverite nastavitve televizorja, da ga pooblastite.", + "not_found": "V omre\u017eju ni bilo najdenih nobenih podprtih naprav Samsung TV.", + "not_successful": "Povezave s to napravo Samsung TV ni mogo\u010de vzpostaviti.", + "not_supported": "Ta naprava Samsung TV trenutno ni podprta." + }, + "flow_title": "Samsung TV: {model}", + "step": { + "confirm": { + "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Gostitelj ali IP naslov", + "name": "Ime" + }, + "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/sv.json b/homeassistant/components/smartthings/.translations/sv.json index 6da4624fa39..725957682ad 100644 --- a/homeassistant/components/smartthings/.translations/sv.json +++ b/homeassistant/components/smartthings/.translations/sv.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "access_token": "\u00c5tkomsttoken" + "access_token": "\u00c5tkomstnyckel" }, "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", "title": "Ange personlig \u00e5tkomsttoken" diff --git a/homeassistant/components/spotify/.translations/sl.json b/homeassistant/components/spotify/.translations/sl.json new file mode 100644 index 00000000000..6ab0b0a40a6 --- /dev/null +++ b/homeassistant/components/spotify/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo en ra\u010dun Spotify.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Integracija Spotify ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjena s Spotify." + }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/en.json b/homeassistant/components/vilfo/.translations/en.json new file mode 100644 index 00000000000..e6b9817f5a8 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "This Vilfo Router is already configured." + }, + "error": { + "cannot_connect": "Failed to connect. Please check the information you provided and try again.", + "invalid_auth": "Invalid authentication. Please check the access token and try again.", + "unknown": "An unexpected error occurred while setting up the integration." + }, + "step": { + "user": { + "data": { + "access_token": "Access token for the Vilfo Router API", + "host": "Router hostname or IP" + }, + "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", + "title": "Connect to the Vilfo Router" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/sv.json b/homeassistant/components/vilfo/.translations/sv.json new file mode 100644 index 00000000000..69edce6b9d8 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r Vilfo-routern \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Kunde inte ansluta. V\u00e4nligen kontrollera informationen du angav och f\u00f6rs\u00f6k igen.", + "invalid_auth": "Ogiltig autentisering. V\u00e4nligen kontrollera \u00e5tkomstnyckeln och f\u00f6rs\u00f6k igen.", + "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade n\u00e4r integrationen skulle konfigureras." + }, + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel f\u00f6r Vilfo-routerns API", + "host": "Routerns v\u00e4rdnamn eller IP-adress" + }, + "description": "St\u00e4ll in Vilfo Router-integrationen. Du beh\u00f6ver din Vilfo-routers v\u00e4rdnamn eller IP-adress och en \u00e5tkomstnyckel till dess API. F\u00f6r ytterligare information om den h\u00e4r integrationen och hur du f\u00e5r fram den n\u00f6dv\u00e4ndiga informationen, bes\u00f6k: https://www.home-assistant.io/integrations/vilfo", + "title": "Anslut till Vilfo-routern" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index ed8a9386072..e069fdac554 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_in_progress": "El flux de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.", "already_setup": "Aquesta entrada ja ha estat configurada.", "already_setup_with_diff_host_and_name": "Sembla que aquesta entrada ja s'ha configurat amb un amfitri\u00f3 i nom diferents a partir del n\u00famero de s\u00e8rie. Elimina les entrades antigues de configuraction.yaml i del men\u00fa d'integracions abans de provar d'afegir el dispositiu novament.", "host_exists": "Ja existeix un component Vizio configurat amb el host.", diff --git a/homeassistant/components/vizio/.translations/sl.json b/homeassistant/components/vizio/.translations/sl.json new file mode 100644 index 00000000000..55faaaf26a8 --- /dev/null +++ b/homeassistant/components/vizio/.translations/sl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfiguracijski tok za komponento vizio je \u017ee v teku.", + "already_setup": "Ta vnos je \u017ee nastavljen.", + "already_setup_with_diff_host_and_name": "Zdi se, da je bil ta vnos \u017ee nastavljen z drugim gostiteljem in imenom glede na njegovo serijsko \u0161tevilko. Pred ponovnim poskusom dodajanja te naprave, odstranite vse stare vnose iz config.yaml in iz menija Integrations.", + "host_exists": "VIZIO komponenta z gostiteljem \u017ee nastavljen.", + "name_exists": "Vizio komponenta z imenom je \u017ee konfigurirana.", + "updated_entry": "Ta vnos je \u017ee nastavljen, vendar se ime in / ali mo\u017enosti, opredeljene v config, ne ujemajo s predhodno uvo\u017eenim configom, zato je bil vnos konfiguracije ustrezno posodobljen.", + "updated_options": "Ta vnos je \u017ee nastavljen, vendar se mo\u017enosti, definirane v config-u, ne ujemajo s predhodno uvo\u017eenimi vrednostmi, zato je bil vnos konfiguracije ustrezno posodobljen.", + "updated_volume_step": "Ta vnos je \u017ee nastavljen, vendar velikost koraka glasnosti v config-u ne ustreza vnosu konfiguracije, zato je bil vnos konfiguracije ustrezno posodobljen." + }, + "error": { + "cant_connect": "Ni bilo mogo\u010de povezati z napravo. [Preglejte dokumente] (https://www.home-assistant.io/integrations/vizio/) in ponovno preverite, ali: \n \u2013 Naprava je vklopljena \n \u2013 Naprava je povezana z omre\u017ejem \n \u2013 Vrednosti, ki ste jih izpolnili, so to\u010dne \nnato poskusite ponovno.", + "host_exists": "Naprava Vizio z dolo\u010denim gostiteljem je \u017ee konfigurirana.", + "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana.", + "tv_needs_token": "Ko je vrsta naprave\u00bb TV \u00ab, je potreben veljaven \u017eeton za dostop." + }, + "step": { + "user": { + "data": { + "access_token": "\u017deton za dostop", + "device_class": "Vrsta naprave", + "host": ":", + "name": "Ime" + }, + "title": "Nastavite odjemalec Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u010casovna omejitev zahteve za API (sekunde)", + "volume_step": "Velikost koraka glasnosti" + }, + "title": "Posodobite mo\u017enosti Vizo SmartCast" + } + }, + "title": "Posodobite mo\u017enosti Vizo SmartCast" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 2ee52b29b2d..600b2dbf450 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo.", "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + }, "profile": { "data": { "profile": "Profil" From d66123cc37592c03cbe42eab53ba189b85410075 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Feb 2020 14:24:15 +0100 Subject: [PATCH 228/378] Fix spelling of ASUSWRT in manifest (#31764) * Fix spelling of ASUSWRT in manifest * Update homeassistant/components/asuswrt/__init__.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- homeassistant/components/asuswrt/__init__.py | 2 +- homeassistant/components/asuswrt/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 64d2d7c7a4b..897258c6299 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -70,7 +70,7 @@ async def async_setup(hass, config): await api.connection.async_connect() if not api.is_connected: - _LOGGER.error("Unable to setup asuswrt component") + _LOGGER.error("Unable to setup component") return False hass.data[DATA_ASUSWRT] = api diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 02999ada68b..416144b450c 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,6 +1,6 @@ { "domain": "asuswrt", - "name": "Asuswrt", + "name": "ASUSWRT", "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": ["aioasuswrt==1.1.22"], "dependencies": [], From 8356ea2616d83802e02c501c92c94b3151b072a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20=C3=98stergaard=20Nielsen?= Date: Thu, 13 Feb 2020 16:12:02 +0100 Subject: [PATCH 229/378] Update to version 2.6 of ihcsdk (#31789) * Make the set_runtime_value_int function work with template values * Use newest version of the ihcsdk library * Make the set_runtime_value_int function work with template values * Use newest version of the ihcsdk library * Updated to the newest ihcsdk 2.5.0 * Formatted changes to make it pass CI tests * Update to version 2.6 of ihcsdk --- homeassistant/components/ihc/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index ac9f2f60218..559ed7c9060 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", - "ihcsdk==2.5.0" + "ihcsdk==2.6.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 6a1699d88fb..ac811f0889f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ ibmiotf==0.3.4 iglo==1.2.7 # homeassistant.components.ihc -ihcsdk==2.5.0 +ihcsdk==2.6.0 # homeassistant.components.incomfort incomfort-client==0.4.0 From 0173c61fee0726f01f77a5178457224d406881eb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Feb 2020 17:27:00 +0100 Subject: [PATCH 230/378] Spelling: Config(uration) (#31782) --- homeassistant/components/axis/strings.json | 52 +++++++++---------- homeassistant/components/bom/sensor.py | 2 +- homeassistant/components/config/manifest.json | 2 +- .../components/ecobee/config_flow.py | 2 +- .../eddystone_temperature/sensor.py | 2 +- homeassistant/components/emoncms/sensor.py | 2 +- homeassistant/components/harmony/remote.py | 2 +- .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/strings.json | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/octoprint/sensor.py | 2 +- .../components/pandora/media_player.py | 4 +- homeassistant/components/plex/strings.json | 2 +- .../components/smartthings/__init__.py | 6 +-- .../components/sonos/media_player.py | 2 +- homeassistant/components/vizio/strings.json | 2 +- homeassistant/components/zwave/__init__.py | 10 ++-- homeassistant/config_entries.py | 8 +-- homeassistant/loader.py | 2 +- tests/auth/test_init.py | 4 +- 20 files changed, 56 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index b50a5c546b8..04a9f9e388a 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -1,29 +1,29 @@ { - "config": { - "title": "Axis device", - "flow_title": "Axis device: {name} ({host})", - "step": { - "user": { - "title": "Set up Axis device", - "data": { - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port" - } - } - }, - "error": { - "already_configured": "Device is already configured", - "already_in_progress": "Config flow for device is already in progress.", - "device_unavailable": "Device is not available", - "faulty_credentials": "Bad user credentials" - }, - "abort": { - "already_configured": "Device is already configured", - "bad_config_file": "Bad data from config file", - "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device" + "config": { + "title": "Axis device", + "flow_title": "Axis device: {name} ({host})", + "step": { + "user": { + "title": "Set up Axis device", + "data": { + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port" } + } + }, + "error": { + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "device_unavailable": "Device is not available", + "faulty_credentials": "Bad user credentials" + }, + "abort": { + "already_configured": "Device is already configured", + "bad_config_file": "Bad data from configuration file", + "link_local_address": "Link local addresses are not supported", + "not_axis_device": "Discovered device not an Axis device" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 4728e22c877..bd57e20edaa 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -112,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if station is not None: if zone_id and wmo_id: _LOGGER.warning( - "Using config %s, not %s and %s for BOM sensor", + "Using configuration %s, not %s and %s for BOM sensor", CONF_STATION, CONF_ZONE_ID, CONF_WMO_ID, diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 809db4ffecc..5d5db4b0741 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -1,6 +1,6 @@ { "domain": "config", - "name": "Config", + "name": "Configuration", "documentation": "https://www.home-assistant.io/integrations/config", "requirements": [], "dependencies": ["http"], diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index bb406d81e3a..cbe16832a34 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -107,7 +107,7 @@ class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if await self.hass.async_add_executor_job(ecobee.refresh_tokens): # Credentials found and validated; create the entry. _LOGGER.debug( - "Valid ecobee configuration found for import, creating config entry" + "Valid ecobee configuration found for import, creating configuration entry" ) return self.async_create_entry( title=DOMAIN, diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 22d3533d32f..3b5aa95701f 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -91,7 +91,7 @@ def get_from_conf(config, config_key, length): string = config.get(config_key) if len(string) != length: _LOGGER.error( - "Error in config parameter %s: Must be exactly %d " + "Error in configuration parameter %s: Must be exactly %d " "bytes. Device will not be added", config_key, length / 2, diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 34063e4c253..f956d3a7295 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -246,7 +246,7 @@ class EmonCmsData: self.data = req.json() else: _LOGGER.error( - "Please verify if the specified config value " + "Please verify if the specified configuration value " "'%s' is correct! (HTTP Status_code = %d)", CONF_URL, req.status_code, diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index c48d5fb00b0..bcc9d72ad08 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -397,7 +397,7 @@ class HarmonyRemote(remote.RemoteDevice): def write_config_file(self): """Write Harmony configuration file.""" _LOGGER.debug( - "%s: Writing hub config to file: %s", self.name, self._config_path + "%s: Writing hub configuration to file: %s", self.name, self._config_path ) if self._client.config is None: _LOGGER.warning("%s: No configuration received from hub", self.name) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 507a5cbb70a..7560f0d3a5d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -262,7 +262,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): _LOGGER.info( ( "Legacy configuration %s for homekit" - "accessory migrated to config entries" + "accessory migrated to configuration entries" ), hkid, ) diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index b51dcb1f6d8..55718e35b59 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -32,7 +32,7 @@ "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", "already_configured": "Accessory is already configured with this controller.", - "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.", + "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", "already_in_progress": "Config flow for device is already in progress." } diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f64c643f0f4..6780a33c7d7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -568,7 +568,7 @@ async def async_setup_entry(hass, entry): conf = CONFIG_SCHEMA({DOMAIN: entry.data})[DOMAIN] elif any(key in conf for key in entry.data): _LOGGER.warning( - "Data in your config entry is going to override your " + "Data in your configuration entry is going to override your " "configuration.yaml: %s", entry.data, ) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index d21aac9ff65..98d878fc2ea 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "If you do not want to have your printer on
" " at all times, and you would like to monitor
" "temperatures, please add
" - "bed and/or number_of_tools to your config
" + "bed and/or number_of_tools to your configuration
" "and restart.", title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 417903c46e0..07f697a5a46 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -119,8 +119,8 @@ class PandoraMediaPlayer(MediaPlayerDevice): elif mode == 2: _LOGGER.warning( "The pianobar client is not configured to log in. " - "Please create a config file for it as described at " - "https://home-assistant.io/components/media_player.pandora/" + "Please create a configuration file for it as described at " + "https://home-assistant.io/integrations/pandora/" ) # pass through the email/password prompts to quit cleanly self._pianobar.sendcontrol("m") diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index b6491db350c..39abbcf9c6f 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -23,7 +23,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "discovery_no_file": "No legacy config file found", + "discovery_no_file": "No legacy configuration file found", "invalid_import": "Imported configuration is invalid", "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 33f9558023d..1539fa076e4 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): except ClientResponseError as ex: if ex.status in (401, 403): _LOGGER.exception( - "Unable to setup config entry '%s' - please reconfigure the integration", + "Unable to setup configuration entry '%s' - please reconfigure the integration", entry.title, ) remove_entry = True @@ -183,7 +183,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): except ClientResponseError as ex: if ex.status == 403: _LOGGER.exception( - "Unable to load scenes for config entry '%s' because the access token does not have the required access", + "Unable to load scenes for configuration entry '%s' because the access token does not have the required access", entry.title, ) else: @@ -230,7 +230,7 @@ async def async_remove_entry(hass: HomeAssistantType, entry: ConfigEntry) -> Non app_count = sum(1 for entry in all_entries if entry.data[CONF_APP_ID] == app_id) if app_count > 1: _LOGGER.debug( - "App %s was not removed because it is in use by other config entries", + "App %s was not removed because it is in use by other configuration entries", app_id, ) return diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 97d03e2116e..84920b6c94d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -107,7 +107,7 @@ class SonosData: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Sonos platform. Obsolete.""" _LOGGER.error( - "Loading Sonos by media_player platform config is no longer supported" + "Loading Sonos by media_player platform configuration is no longer supported" ) diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 64b2fb5f936..5a554b7e3db 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -21,7 +21,7 @@ "abort": { "already_setup": "This entry has already been setup.", "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", - "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly." + "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." } }, "options": { diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 9b9236de1c2..ba7e26ee58c 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -678,7 +678,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_BOOL: value.data = int(selection == "True") _LOGGER.info( - "Setting config parameter %s on Node %s with bool selection %s", + "Setting configuration parameter %s on Node %s with bool selection %s", param, node_id, str(selection), @@ -687,7 +687,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_LIST: value.data = str(selection) _LOGGER.info( - "Setting config parameter %s on Node %s with list selection %s", + "Setting configuration parameter %s on Node %s with list selection %s", param, node_id, str(selection), @@ -697,7 +697,7 @@ async def async_setup_entry(hass, config_entry): network.manager.pressButton(value.value_id) network.manager.releaseButton(value.value_id) _LOGGER.info( - "Setting config parameter %s on Node %s " + "Setting configuration parameter %s on Node %s " "with button selection %s", param, node_id, @@ -706,7 +706,7 @@ async def async_setup_entry(hass, config_entry): return value.data = int(selection) _LOGGER.info( - "Setting config parameter %s on Node %s with selection %s", + "Setting configuration parameter %s on Node %s with selection %s", param, node_id, selection, @@ -714,7 +714,7 @@ async def async_setup_entry(hass, config_entry): return node.set_config_param(param, selection, size) _LOGGER.info( - "Setting unknown config parameter %s on Node %s with selection %s", + "Setting unknown configuration parameter %s on Node %s with selection %s", param, node_id, selection, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d8e64172ba0..35631225bfb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -183,7 +183,7 @@ class ConfigEntry: component = integration.get_component() except ImportError as err: _LOGGER.error( - "Error importing integration %s to set up %s config entry: %s", + "Error importing integration %s to set up %s configuration entry: %s", integration.domain, self.domain, err, @@ -197,7 +197,7 @@ class ConfigEntry: integration.get_platform("config_flow") except ImportError as err: _LOGGER.error( - "Error importing platform config_flow from integration %s to set up %s config entry: %s", + "Error importing platform config_flow from integration %s to set up %s configuration entry: %s", integration.domain, self.domain, err, @@ -503,7 +503,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): integration.get_platform("config_flow") except ImportError as err: _LOGGER.error( - "Error occurred loading config flow for integration %s: %s", + "Error occurred loading configuration flow for integration %s: %s", handler_key, err, ) @@ -1024,7 +1024,7 @@ class EntityRegistryDisabledHandler: self.changed = set() _LOGGER.info( - "Reloading config entries because disabled_by changed in entity registry: %s", + "Reloading configuration entries because disabled_by changed in entity registry: %s", ", ".join(self.changed), ) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 0f69f4600b2..9033202e652 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -541,7 +541,7 @@ def _async_mount_config_dir(hass: "HomeAssistant") -> bool: Async friendly but not a coroutine. """ if hass.config.config_dir is None: - _LOGGER.error("Can't load integrations - config dir is not set") + _LOGGER.error("Can't load integrations - configuration directory is not set") return False if hass.config.config_dir not in sys.path: sys.path.insert(0, hass.config.config_dir) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 9d93e5e7042..82c0c0dbdbd 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -31,7 +31,7 @@ async def test_auth_manager_from_config_validates_config(mock_hass): [ {"name": "Test Name", "type": "insecure_example", "users": []}, { - "name": "Invalid config because no users", + "name": "Invalid configuration because no users", "type": "insecure_example", "id": "invalid_config", }, @@ -81,7 +81,7 @@ async def test_auth_manager_from_config_auth_modules(mock_hass): [ {"name": "Module 1", "type": "insecure_example", "data": []}, { - "name": "Invalid config because no data", + "name": "Invalid configuration because no data", "type": "insecure_example", "id": "another", }, From 3e23a3a860882b938c1d9f815752f58e9f0990f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 13 Feb 2020 18:52:58 +0200 Subject: [PATCH 231/378] Add and use bunch of data size and rate related constants (#31781) Also fix a few units to match the corresponding data. --- homeassistant/components/asuswrt/sensor.py | 9 ++-- homeassistant/components/bbox/sensor.py | 11 +++-- homeassistant/components/deluge/sensor.py | 5 ++- homeassistant/components/dovado/sensor.py | 11 +++-- homeassistant/components/ebox/sensor.py | 26 ++++++----- homeassistant/components/fastdotcom/sensor.py | 5 +-- homeassistant/components/fido/sensor.py | 8 ++-- homeassistant/components/filesize/sensor.py | 3 +- homeassistant/components/folder/sensor.py | 3 +- homeassistant/components/freebox/sensor.py | 5 ++- homeassistant/components/glances/const.py | 21 +++++---- homeassistant/components/huawei_lte/const.py | 1 - homeassistant/components/huawei_lte/sensor.py | 11 +++-- homeassistant/components/iperf3/__init__.py | 7 ++- .../components/netgear_lte/sensor_types.py | 3 +- homeassistant/components/nzbget/sensor.py | 17 ++++--- homeassistant/components/nzbget/services.yaml | 4 +- homeassistant/components/pyload/sensor.py | 3 +- .../components/qbittorrent/sensor.py | 5 ++- homeassistant/components/qnap/sensor.py | 20 +++++---- homeassistant/components/radarr/sensor.py | 25 +++++++++-- homeassistant/components/rtorrent/sensor.py | 5 ++- homeassistant/components/sabnzbd/__init__.py | 21 +++++---- homeassistant/components/sonarr/sensor.py | 25 +++++++++-- .../components/speedtestdotnet/const.py | 6 ++- homeassistant/components/startca/sensor.py | 30 +++++++------ .../components/synologydsm/sensor.py | 18 ++++---- .../components/systemmonitor/sensor.py | 30 ++++++++----- homeassistant/components/teksavvy/sensor.py | 26 ++++++----- .../components/transmission/const.py | 7 ++- homeassistant/components/vultr/sensor.py | 4 +- homeassistant/components/wled/const.py | 1 - homeassistant/components/wled/sensor.py | 11 +---- homeassistant/const.py | 34 ++++++++++++++ tests/components/radarr/test_sensor.py | 23 +++++----- tests/components/rest/test_sensor.py | 7 +-- tests/components/sonarr/test_sensor.py | 27 +++++------ tests/components/startca/test_sensor.py | 45 ++++++++++--------- tests/components/teksavvy/test_sensor.py | 37 +++++++-------- tests/components/vultr/test_sensor.py | 9 +++- tests/components/wled/test_sensor.py | 3 +- 41 files changed, 349 insertions(+), 223 deletions(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index b5ce8539f44..50100d3625d 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,6 +1,7 @@ """Asuswrt status sensors.""" import logging +from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.helpers.entity import Entity from . import DATA_ASUSWRT @@ -61,7 +62,7 @@ class AsuswrtRXSensor(AsuswrtSensor): """Representation of a asuswrt download speed sensor.""" _name = "Asuswrt Download Speed" - _unit = "Mbit/s" + _unit = DATA_RATE_MEGABITS_PER_SECOND @property def unit_of_measurement(self): @@ -79,7 +80,7 @@ class AsuswrtTXSensor(AsuswrtSensor): """Representation of a asuswrt upload speed sensor.""" _name = "Asuswrt Upload Speed" - _unit = "Mbit/s" + _unit = DATA_RATE_MEGABITS_PER_SECOND @property def unit_of_measurement(self): @@ -97,7 +98,7 @@ class AsuswrtTotalRXSensor(AsuswrtSensor): """Representation of a asuswrt total download sensor.""" _name = "Asuswrt Download" - _unit = "Gigabyte" + _unit = DATA_GIGABYTES @property def unit_of_measurement(self): @@ -115,7 +116,7 @@ class AsuswrtTotalTXSensor(AsuswrtSensor): """Representation of a asuswrt total upload sensor.""" _name = "Asuswrt Upload" - _unit = "Gigabyte" + _unit = DATA_GIGABYTES @property def unit_of_measurement(self): diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index f5e5865f6f0..259066d4561 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_MONITORED_VARIABLES, CONF_NAME, + DATA_RATE_MEGABITS_PER_SECOND, DEVICE_CLASS_TIMESTAMP, ) import homeassistant.helpers.config_validation as cv @@ -20,8 +21,6 @@ from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) -BANDWIDTH_MEGABITS_SECONDS = "Mb/s" - ATTRIBUTION = "Powered by Bouygues Telecom" DEFAULT_NAME = "Bbox" @@ -32,22 +31,22 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { "down_max_bandwidth": [ "Maximum Download Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:download", ], "up_max_bandwidth": [ "Maximum Upload Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:upload", ], "current_down_bandwidth": [ "Currently Used Download Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:download", ], "current_up_bandwidth": [ "Currently Used Upload Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:upload", ], "uptime": ["Uptime", None, "mdi:clock"], diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 7df87490c60..55309ea8b31 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, ) from homeassistant.exceptions import PlatformNotReady @@ -27,8 +28,8 @@ DHT_UPLOAD = 1000 DHT_DOWNLOAD = 1000 SENSOR_TYPES = { "current_status": ["Status", None], - "download_speed": ["Down Speed", "kB/s"], - "upload_speed": ["Up Speed", "kB/s"], + "download_speed": ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], + "upload_speed": ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index d3374c8d02a..ab85c376469 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -6,7 +6,7 @@ import re import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_SENSORS +from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -26,8 +26,13 @@ SENSORS = { SENSOR_NETWORK: ("signal strength", "Network", None, "mdi:access-point-network"), SENSOR_SIGNAL: ("signal strength", "Signal Strength", "%", "mdi:signal"), SENSOR_SMS_UNREAD: ("sms unread", "SMS unread", "", "mdi:message-text-outline"), - SENSOR_UPLOAD: ("traffic modem tx", "Sent", "GB", "mdi:cloud-upload"), - SENSOR_DOWNLOAD: ("traffic modem rx", "Received", "GB", "mdi:cloud-download"), + SENSOR_UPLOAD: ("traffic modem tx", "Sent", DATA_GIGABYTES, "mdi:cloud-upload"), + SENSOR_DOWNLOAD: ( + "traffic modem rx", + "Received", + DATA_GIGABYTES, + "mdi:cloud-download", + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 55504e8edf7..54355ed3bb8 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DATA_GIGABITS, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -27,7 +28,6 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -GIGABITS = "Gb" PRICE = "CAD" DAYS = "days" PERCENT = "%" @@ -41,17 +41,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { "usage": ["Usage", PERCENT, "mdi:percent"], "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "limit": ["Data limit", GIGABITS, "mdi:download"], + "limit": ["Data limit", DATA_GIGABITS, "mdi:download"], "days_left": ["Days left", DAYS, "mdi:calendar-today"], - "before_offpeak_download": ["Download before offpeak", GIGABITS, "mdi:download"], - "before_offpeak_upload": ["Upload before offpeak", GIGABITS, "mdi:upload"], - "before_offpeak_total": ["Total before offpeak", GIGABITS, "mdi:download"], - "offpeak_download": ["Offpeak download", GIGABITS, "mdi:download"], - "offpeak_upload": ["Offpeak Upload", GIGABITS, "mdi:upload"], - "offpeak_total": ["Offpeak Total", GIGABITS, "mdi:download"], - "download": ["Download", GIGABITS, "mdi:download"], - "upload": ["Upload", GIGABITS, "mdi:upload"], - "total": ["Total", GIGABITS, "mdi:download"], + "before_offpeak_download": [ + "Download before offpeak", + DATA_GIGABITS, + "mdi:download", + ], + "before_offpeak_upload": ["Upload before offpeak", DATA_GIGABITS, "mdi:upload"], + "before_offpeak_total": ["Total before offpeak", DATA_GIGABITS, "mdi:download"], + "offpeak_download": ["Offpeak download", DATA_GIGABITS, "mdi:download"], + "offpeak_upload": ["Offpeak Upload", DATA_GIGABITS, "mdi:upload"], + "offpeak_total": ["Offpeak Total", DATA_GIGABITS, "mdi:download"], + "download": ["Download", DATA_GIGABITS, "mdi:download"], + "upload": ["Upload", DATA_GIGABITS, "mdi:upload"], + "total": ["Total", DATA_GIGABITS, "mdi:download"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 6d9445ce159..a6eaa21ae35 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -1,6 +1,7 @@ """Support for Fast.com internet speed testing sensor.""" import logging +from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity @@ -11,8 +12,6 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:speedometer" -UNIT_OF_MEASUREMENT = "Mbit/s" - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Fast.com sensor.""" @@ -41,7 +40,7 @@ class SpeedtestSensor(RestoreEntity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return UNIT_OF_MEASUREMENT + return DATA_RATE_MEGABITS_PER_SECOND @property def icon(self): diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 086ae87a529..f444abd25ee 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DATA_KILOBITS, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -27,7 +28,6 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -KILOBITS = "Kb" PRICE = "CAD" MESSAGES = "messages" MINUTES = "minutes" @@ -40,9 +40,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { "fido_dollar": ["Fido dollar", PRICE, "mdi:square-inc-cash"], "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "data_used": ["Data used", KILOBITS, "mdi:download"], - "data_limit": ["Data limit", KILOBITS, "mdi:download"], - "data_remaining": ["Data remaining", KILOBITS, "mdi:download"], + "data_used": ["Data used", DATA_KILOBITS, "mdi:download"], + "data_limit": ["Data limit", DATA_KILOBITS, "mdi:download"], + "data_remaining": ["Data remaining", DATA_KILOBITS, "mdi:download"], "text_used": ["Text used", MESSAGES, "mdi:message-text"], "text_limit": ["Text limit", MESSAGES, "mdi:message-text"], "text_remaining": ["Text remaining", MESSAGES, "mdi:message-text"], diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 8c6cd30b118..3d96aab04e9 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -6,6 +6,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -42,7 +43,7 @@ class Filesize(Entity): self._size = None self._last_updated = None self._name = path.split("/")[-1] - self._unit_of_measurement = "MB" + self._unit_of_measurement = DATA_MEGABYTES def update(self): """Update the sensor.""" diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index a706ab2a0b5..19a5791d7cb 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -7,6 +7,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -63,7 +64,7 @@ class Folder(Entity): self._number_of_files = None self._size = None self._name = os.path.split(os.path.split(folder_path)[0])[1] - self._unit_of_measurement = "MB" + self._unit_of_measurement = DATA_MEGABYTES self._file_list = None def update(self): diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 61ec670d217..0653120b49c 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,6 +1,7 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging +from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND from homeassistant.helpers.entity import Entity from . import DATA_FREEBOX @@ -56,7 +57,7 @@ class FbxRXSensor(FbxSensor): """Update the Freebox RxSensor.""" _name = "Freebox download speed" - _unit = "KB/s" + _unit = DATA_RATE_KILOBYTES_PER_SECOND _icon = "mdi:download-network" async def async_update(self): @@ -70,7 +71,7 @@ class FbxTXSensor(FbxSensor): """Update the Freebox TxSensor.""" _name = "Freebox upload speed" - _unit = "KB/s" + _unit = DATA_RATE_KILOBYTES_PER_SECOND _icon = "mdi:upload-network" async def async_update(self): diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index b7f5a2d642b..31a3f0f69e4 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,5 +1,5 @@ """Constants for Glances component.""" -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, TEMP_CELSIUS DOMAIN = "glances" CONF_VERSION = "version" @@ -15,14 +15,14 @@ SUPPORTED_VERSIONS = [2, 3] SENSOR_TYPES = { "disk_use_percent": ["fs", "used percent", "%", "mdi:harddisk"], - "disk_use": ["fs", "used", "GiB", "mdi:harddisk"], - "disk_free": ["fs", "free", "GiB", "mdi:harddisk"], + "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk"], + "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk"], "memory_use_percent": ["mem", "RAM used percent", "%", "mdi:memory"], - "memory_use": ["mem", "RAM used", "MiB", "mdi:memory"], - "memory_free": ["mem", "RAM free", "MiB", "mdi:memory"], + "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory"], + "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory"], "swap_use_percent": ["memswap", "Swap used percent", "%", "mdi:memory"], - "swap_use": ["memswap", "Swap used", "GiB", "mdi:memory"], - "swap_free": ["memswap", "Swap free", "GiB", "mdi:memory"], + "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory"], + "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory"], "processor_load": ["load", "CPU load", "15 min", "mdi:memory"], "process_running": ["processcount", "Running", "Count", "mdi:memory"], "process_total": ["processcount", "Total", "Count", "mdi:memory"], @@ -32,5 +32,10 @@ SENSOR_TYPES = { "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"], "docker_active": ["docker", "Containers active", "", "mdi:docker"], "docker_cpu_use": ["docker", "Containers CPU used", "%", "mdi:docker"], - "docker_memory_use": ["docker", "Containers RAM used", "MiB", "mdi:docker"], + "docker_memory_use": [ + "docker", + "Containers RAM used", + DATA_MEBIBYTES, + "mdi:docker", + ], } diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index e227f06cf28..6d699420283 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -8,7 +8,6 @@ DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" -UNIT_BYTES = "B" UNIT_SECONDS = "s" CONNECTION_TIMEOUT = 10 diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 3b6b75edfba..54c5441c6e2 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.const import CONF_URL, STATE_UNKNOWN +from homeassistant.const import CONF_URL, DATA_BYTES, STATE_UNKNOWN from . import HuaweiLteBaseEntity from .const import ( @@ -18,7 +18,6 @@ from .const import ( KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_TRAFFIC_STATISTICS, - UNIT_BYTES, UNIT_SECONDS, ) @@ -126,19 +125,19 @@ SENSOR_META = { name="Current connection duration", unit=UNIT_SECONDS, icon="mdi:timer" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( - name="Current connection download", unit=UNIT_BYTES, icon="mdi:download" + name="Current connection download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict( - name="Current connection upload", unit=UNIT_BYTES, icon="mdi:upload" + name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( name="Total connected duration", unit=UNIT_SECONDS, icon="mdi:timer" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict( - name="Total download", unit=UNIT_BYTES, icon="mdi:download" + name="Total download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict( - name="Total upload", unit=UNIT_BYTES, icon="mdi:upload" + name="Total upload", unit=DATA_BYTES, icon="mdi:upload" ), } diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 9272a725bb7..bd5aeac099a 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_SCAN_INTERVAL, + DATA_RATE_MEGABITS_PER_SECOND, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -39,11 +40,9 @@ ATTR_UPLOAD = "upload" ATTR_VERSION = "Version" ATTR_HOST = "host" -UNIT_OF_MEASUREMENT = "Mbit/s" - SENSOR_TYPES = { - ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), UNIT_OF_MEASUREMENT], - ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), UNIT_OF_MEASUREMENT], + ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), DATA_RATE_MEGABITS_PER_SECOND], + ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), DATA_RATE_MEGABITS_PER_SECOND], } PROTOCOLS = ["tcp", "udp"] diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py index e1a9d1a23d2..a744937dacd 100644 --- a/homeassistant/components/netgear_lte/sensor_types.py +++ b/homeassistant/components/netgear_lte/sensor_types.py @@ -1,6 +1,7 @@ """Define possible sensor types.""" from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.const import DATA_MEBIBYTES SENSOR_SMS = "sms" SENSOR_SMS_TOTAL = "sms_total" @@ -9,7 +10,7 @@ SENSOR_USAGE = "usage" SENSOR_UNITS = { SENSOR_SMS: "unread", SENSOR_SMS_TOTAL: "messages", - SENSOR_USAGE: "MiB", + SENSOR_USAGE: DATA_MEBIBYTES, "radio_quality": "%", "rx_level": "dBm", "tx_level": "dBm", diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 3556c88a6da..24ef18ab985 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,6 +1,7 @@ """Monitor the NZBGet API.""" import logging +from homeassistant.const import DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -12,15 +13,19 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "NZBGet" SENSOR_TYPES = { - "article_cache": ["ArticleCacheMB", "Article Cache", "MB"], - "average_download_rate": ["AverageDownloadRate", "Average Speed", "MB/s"], + "article_cache": ["ArticleCacheMB", "Article Cache", DATA_MEGABYTES], + "average_download_rate": [ + "AverageDownloadRate", + "Average Speed", + DATA_RATE_MEGABYTES_PER_SECOND, + ], "download_paused": ["DownloadPaused", "Download Paused", None], - "download_rate": ["DownloadRate", "Speed", "MB/s"], - "download_size": ["DownloadedSizeMB", "Size", "MB"], - "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", "MB"], + "download_rate": ["DownloadRate", "Speed", DATA_RATE_MEGABYTES_PER_SECOND], + "download_size": ["DownloadedSizeMB", "Size", DATA_MEGABYTES], + "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", DATA_MEGABYTES], "post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"], "post_paused": ["PostPaused", "Post Processing Paused", None], - "remaining_size": ["RemainingSizeMB", "Queue Size", "MB"], + "remaining_size": ["RemainingSizeMB", "Queue Size", DATA_MEGABYTES], "uptime": ["UpTimeSec", "Uptime", "min"], } diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 84e4af15b5d..88a6267860e 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -10,5 +10,5 @@ set_speed: description: Set download speed limit fields: speed: - description: Speed limit in KB/s. 0 is unlimited. - example: 1000 \ No newline at end of file + description: Speed limit in kB/s. 0 is unlimited. + example: 1000 diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index fd4461e3e1b..579919821a3 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONTENT_TYPE_JSON, + DATA_RATE_MEGABYTES_PER_SECOND, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -29,7 +30,7 @@ DEFAULT_PORT = 8000 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) -SENSOR_TYPES = {"speed": ["speed", "Speed", "MB/s"]} +SENSOR_TYPES = {"speed": ["speed", "Speed", DATA_RATE_MEGABYTES_PER_SECOND]} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 9544d74b1cd..46f82e99a62 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_URL, CONF_USERNAME, + DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, ) from homeassistant.exceptions import PlatformNotReady @@ -27,8 +28,8 @@ DEFAULT_NAME = "qBittorrent" SENSOR_TYPES = { SENSOR_TYPE_CURRENT_STATUS: ["Status", None], - SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", "kB/s"], - SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", "kB/s"], + SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], + SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index c3863bd0077..1ad53f4db48 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -16,6 +16,8 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, CONF_VERIFY_SSL, + DATA_GIBIBYTES, + DATA_RATE_MEBIBYTES_PER_SECOND, TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady @@ -62,22 +64,22 @@ _CPU_MON_COND = { "cpu_usage": ["CPU Usage", "%", "mdi:chip"], } _MEMORY_MON_COND = { - "memory_free": ["Memory Available", "GB", "mdi:memory"], - "memory_used": ["Memory Used", "GB", "mdi:memory"], + "memory_free": ["Memory Available", DATA_GIBIBYTES, "mdi:memory"], + "memory_used": ["Memory Used", DATA_GIBIBYTES, "mdi:memory"], "memory_percent_used": ["Memory Usage", "%", "mdi:memory"], } _NETWORK_MON_COND = { "network_link_status": ["Network Link", None, "mdi:checkbox-marked-circle-outline"], - "network_tx": ["Network Up", "MB/s", "mdi:upload"], - "network_rx": ["Network Down", "MB/s", "mdi:download"], + "network_tx": ["Network Up", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:upload"], + "network_rx": ["Network Down", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:download"], } _DRIVE_MON_COND = { "drive_smart_status": ["SMART Status", None, "mdi:checkbox-marked-circle-outline"], "drive_temp": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], } _VOLUME_MON_COND = { - "volume_size_used": ["Used Space", "GB", "mdi:chart-pie"], - "volume_size_free": ["Free Space", "GB", "mdi:chart-pie"], + "volume_size_used": ["Used Space", DATA_GIBIBYTES, "mdi:chart-pie"], + "volume_size_free": ["Free Space", DATA_GIBIBYTES, "mdi:chart-pie"], "volume_percentage_used": ["Volume Used", "%", "mdi:chart-pie"], } @@ -270,7 +272,7 @@ class QNAPMemorySensor(QNAPSensor): if self._api.data: data = self._api.data["system_stats"]["memory"] size = round_nicely(float(data["total"]) / 1024) - return {ATTR_MEMORY_SIZE: f"{size} GB"} + return {ATTR_MEMORY_SIZE: f"{size} {DATA_GIBIBYTES}"} class QNAPNetworkSensor(QNAPSensor): @@ -399,4 +401,6 @@ class QNAPVolumeSensor(QNAPSensor): data = self._api.data["volumes"][self.monitor_device] total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 - return {ATTR_VOLUME_SIZE: "{} GB".format(round_nicely(total_gb))} + return { + ATTR_VOLUME_SIZE: "{} {}".format(round_nicely(total_gb), DATA_GIBIBYTES) + } diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 79e45ffd9a8..6cfdd53653d 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -14,6 +14,15 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PORT, CONF_SSL, + DATA_BYTES, + DATA_EXABYTES, + DATA_GIGABYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_PETABYTES, + DATA_TERABYTES, + DATA_YOTTABYTES, + DATA_ZETTABYTES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -29,12 +38,12 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 7878 DEFAULT_URLBASE = "" DEFAULT_DAYS = "1" -DEFAULT_UNIT = "GB" +DEFAULT_UNIT = DATA_GIGABYTES SCAN_INTERVAL = timedelta(minutes=10) SENSOR_TYPES = { - "diskspace": ["Disk Space", "GB", "mdi:harddisk"], + "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"], "upcoming": ["Upcoming", "Movies", "mdi:television"], "wanted": ["Wanted", "Movies", "mdi:television"], "movies": ["Movies", "Movies", "mdi:television"], @@ -51,7 +60,17 @@ ENDPOINTS = { } # Support to Yottabytes for the future, why not -BYTE_SIZES = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] +BYTE_SIZES = [ + DATA_BYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_GIGABYTES, + DATA_TERABYTES, + DATA_PETABYTES, + DATA_EXABYTES, + DATA_ZETTABYTES, + DATA_YOTTABYTES, +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 4ae272ca9bd..c6833fcfda0 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, CONF_URL, + DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, ) from homeassistant.exceptions import PlatformNotReady @@ -24,8 +25,8 @@ SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" DEFAULT_NAME = "rtorrent" SENSOR_TYPES = { SENSOR_TYPE_CURRENT_STATUS: ["Status", None], - SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", "kB/s"], - SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", "kB/s"], + SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], + SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f436bcb8a72..b36abbedb48 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -14,6 +14,9 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, + DATA_GIGABYTES, + DATA_MEGABYTES, + DATA_RATE_MEGABYTES_PER_SECOND, ) from homeassistant.core import callback from homeassistant.helpers import discovery @@ -49,16 +52,16 @@ SIGNAL_SABNZBD_UPDATED = "sabnzbd_updated" SENSOR_TYPES = { "current_status": ["Status", None, "status"], - "speed": ["Speed", "MB/s", "kbpersec"], - "queue_size": ["Queue", "MB", "mb"], - "queue_remaining": ["Left", "MB", "mbleft"], - "disk_size": ["Disk", "GB", "diskspacetotal1"], - "disk_free": ["Disk Free", "GB", "diskspace1"], + "speed": ["Speed", DATA_RATE_MEGABYTES_PER_SECOND, "kbpersec"], + "queue_size": ["Queue", DATA_MEGABYTES, "mb"], + "queue_remaining": ["Left", DATA_MEGABYTES, "mbleft"], + "disk_size": ["Disk", DATA_GIGABYTES, "diskspacetotal1"], + "disk_free": ["Disk Free", DATA_GIGABYTES, "diskspace1"], "queue_count": ["Queue Count", None, "noofslots_total"], - "day_size": ["Daily Total", "GB", "day_size"], - "week_size": ["Weekly Total", "GB", "week_size"], - "month_size": ["Monthly Total", "GB", "month_size"], - "total_size": ["Total", "GB", "total_size"], + "day_size": ["Daily Total", DATA_GIGABYTES, "day_size"], + "week_size": ["Weekly Total", DATA_GIGABYTES, "week_size"], + "month_size": ["Monthly Total", DATA_GIGABYTES, "month_size"], + "total_size": ["Total", DATA_GIGABYTES, "total_size"], } SPEED_LIMIT_SCHEMA = vol.Schema( diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 82bcdad6ef4..c0781b37603 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -14,6 +14,15 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PORT, CONF_SSL, + DATA_BYTES, + DATA_EXABYTES, + DATA_GIGABYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_PETABYTES, + DATA_TERABYTES, + DATA_YOTTABYTES, + DATA_ZETTABYTES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -29,10 +38,10 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 8989 DEFAULT_URLBASE = "" DEFAULT_DAYS = "1" -DEFAULT_UNIT = "GB" +DEFAULT_UNIT = DATA_GIGABYTES SENSOR_TYPES = { - "diskspace": ["Disk Space", "GB", "mdi:harddisk"], + "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"], "queue": ["Queue", "Episodes", "mdi:download"], "upcoming": ["Upcoming", "Episodes", "mdi:television"], "wanted": ["Wanted", "Episodes", "mdi:television"], @@ -52,7 +61,17 @@ ENDPOINTS = { } # Support to Yottabytes for the future, why not -BYTE_SIZES = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] +BYTE_SIZES = [ + DATA_BYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_GIGABYTES, + DATA_TERABYTES, + DATA_PETABYTES, + DATA_EXABYTES, + DATA_ZETTABYTES, + DATA_YOTTABYTES, +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 69aadb7ac6c..a08c9421c76 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,10 +1,12 @@ """Consts used by Speedtest.net.""" +from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND + DOMAIN = "speedtestdotnet" DATA_UPDATED = f"{DOMAIN}_data_updated" SENSOR_TYPES = { "ping": ["Ping", "ms"], - "download": ["Download", "Mbit/s"], - "upload": ["Upload", "Mbit/s"], + "download": ["Download", DATA_RATE_MEGABITS_PER_SECOND], + "upload": ["Upload", DATA_RATE_MEGABITS_PER_SECOND], } diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index e07f21e5d60..82106c2da57 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -8,7 +8,12 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_MONITORED_VARIABLES, + CONF_NAME, + DATA_GIGABYTES, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -19,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Start.ca" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) @@ -27,17 +31,17 @@ REQUEST_TIMEOUT = 5 # seconds SENSOR_TYPES = { "usage": ["Usage Ratio", PERCENT, "mdi:percent"], - "usage_gb": ["Usage", GIGABYTES, "mdi:download"], - "limit": ["Data limit", GIGABYTES, "mdi:download"], - "used_download": ["Used Download", GIGABYTES, "mdi:download"], - "used_upload": ["Used Upload", GIGABYTES, "mdi:upload"], - "used_total": ["Used Total", GIGABYTES, "mdi:download"], - "grace_download": ["Grace Download", GIGABYTES, "mdi:download"], - "grace_upload": ["Grace Upload", GIGABYTES, "mdi:upload"], - "grace_total": ["Grace Total", GIGABYTES, "mdi:download"], - "total_download": ["Total Download", GIGABYTES, "mdi:download"], - "total_upload": ["Total Upload", GIGABYTES, "mdi:download"], - "used_remaining": ["Remaining", GIGABYTES, "mdi:download"], + "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"], + "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"], + "used_download": ["Used Download", DATA_GIGABYTES, "mdi:download"], + "used_upload": ["Used Upload", DATA_GIGABYTES, "mdi:upload"], + "used_total": ["Used Total", DATA_GIGABYTES, "mdi:download"], + "grace_download": ["Grace Download", DATA_GIGABYTES, "mdi:download"], + "grace_upload": ["Grace Upload", DATA_GIGABYTES, "mdi:upload"], + "grace_total": ["Grace Total", DATA_GIGABYTES, "mdi:download"], + "total_download": ["Total Download", DATA_GIGABYTES, "mdi:download"], + "total_upload": ["Total Upload", DATA_GIGABYTES, "mdi:download"], + "used_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index 3f823331433..d10ecaa15ed 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -17,6 +17,8 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, + DATA_MEGABYTES, + DATA_RATE_KILOBYTES_PER_SECOND, EVENT_HOMEASSISTANT_START, TEMP_CELSIUS, ) @@ -43,14 +45,14 @@ _UTILISATION_MON_COND = { "cpu_5min_load": ["CPU Load (5 min)", "%", "mdi:chip"], "cpu_15min_load": ["CPU Load (15 min)", "%", "mdi:chip"], "memory_real_usage": ["Memory Usage (Real)", "%", "mdi:memory"], - "memory_size": ["Memory Size", "Mb", "mdi:memory"], - "memory_cached": ["Memory Cached", "Mb", "mdi:memory"], - "memory_available_swap": ["Memory Available (Swap)", "Mb", "mdi:memory"], - "memory_available_real": ["Memory Available (Real)", "Mb", "mdi:memory"], - "memory_total_swap": ["Memory Total (Swap)", "Mb", "mdi:memory"], - "memory_total_real": ["Memory Total (Real)", "Mb", "mdi:memory"], - "network_up": ["Network Up", "Kbps", "mdi:upload"], - "network_down": ["Network Down", "Kbps", "mdi:download"], + "memory_size": ["Memory Size", DATA_MEGABYTES, "mdi:memory"], + "memory_cached": ["Memory Cached", DATA_MEGABYTES, "mdi:memory"], + "memory_available_swap": ["Memory Available (Swap)", DATA_MEGABYTES, "mdi:memory"], + "memory_available_real": ["Memory Available (Real)", DATA_MEGABYTES, "mdi:memory"], + "memory_total_swap": ["Memory Total (Swap)", DATA_MEGABYTES, "mdi:memory"], + "memory_total_real": ["Memory Total (Real)", DATA_MEGABYTES, "mdi:memory"], + "network_up": ["Network Up", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:upload"], + "network_down": ["Network Down", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:download"], } _STORAGE_VOL_MON_COND = { "volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index b1a33736083..1ea8a409052 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -7,7 +7,15 @@ import psutil import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_RESOURCES, CONF_TYPE, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_RESOURCES, + CONF_TYPE, + DATA_GIBIBYTES, + DATA_MEBIBYTES, + DATA_RATE_MEGABYTES_PER_SECOND, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util @@ -19,8 +27,8 @@ _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" SENSOR_TYPES = { - "disk_free": ["Disk free", "GiB", "mdi:harddisk", None], - "disk_use": ["Disk use", "GiB", "mdi:harddisk", None], + "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None], + "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None], "disk_use_percent": ["Disk use (percent)", "%", "mdi:harddisk", None], "ipv4_address": ["IPv4 address", "", "mdi:server-network", None], "ipv6_address": ["IPv6 address", "", "mdi:server-network", None], @@ -28,29 +36,29 @@ SENSOR_TYPES = { "load_15m": ["Load (15m)", " ", "mdi:memory", None], "load_1m": ["Load (1m)", " ", "mdi:memory", None], "load_5m": ["Load (5m)", " ", "mdi:memory", None], - "memory_free": ["Memory free", "MiB", "mdi:memory", None], - "memory_use": ["Memory use", "MiB", "mdi:memory", None], + "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None], + "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None], "memory_use_percent": ["Memory use (percent)", "%", "mdi:memory", None], - "network_in": ["Network in", "MiB", "mdi:server-network", None], - "network_out": ["Network out", "MiB", "mdi:server-network", None], + "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None], + "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None], "packets_in": ["Packets in", " ", "mdi:server-network", None], "packets_out": ["Packets out", " ", "mdi:server-network", None], "throughput_network_in": [ "Network throughput in", - "MB/s", + DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", None, ], "throughput_network_out": [ "Network throughput out", - "MB/s", + DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", None, ], "process": ["Process", " ", "mdi:memory", None], "processor_use": ["Processor use", "%", "mdi:memory", None], - "swap_free": ["Swap free", "MiB", "mdi:harddisk", None], - "swap_use": ["Swap use", "MiB", "mdi:harddisk", None], + "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None], + "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None], "swap_use_percent": ["Swap use (percent)", "%", "mdi:harddisk", None], } diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index fe183129eaa..f340f4a3971 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -6,7 +6,12 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_MONITORED_VARIABLES, + CONF_NAME, + DATA_GIGABYTES, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "TekSavvy" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) @@ -25,15 +29,15 @@ REQUEST_TIMEOUT = 5 # seconds SENSOR_TYPES = { "usage": ["Usage Ratio", PERCENT, "mdi:percent"], - "usage_gb": ["Usage", GIGABYTES, "mdi:download"], - "limit": ["Data limit", GIGABYTES, "mdi:download"], - "onpeak_download": ["On Peak Download", GIGABYTES, "mdi:download"], - "onpeak_upload": ["On Peak Upload", GIGABYTES, "mdi:upload"], - "onpeak_total": ["On Peak Total", GIGABYTES, "mdi:download"], - "offpeak_download": ["Off Peak download", GIGABYTES, "mdi:download"], - "offpeak_upload": ["Off Peak Upload", GIGABYTES, "mdi:upload"], - "offpeak_total": ["Off Peak Total", GIGABYTES, "mdi:download"], - "onpeak_remaining": ["Remaining", GIGABYTES, "mdi:download"], + "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"], + "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"], + "onpeak_download": ["On Peak Download", DATA_GIGABYTES, "mdi:download"], + "onpeak_upload": ["On Peak Upload", DATA_GIGABYTES, "mdi:upload"], + "onpeak_total": ["On Peak Total", DATA_GIGABYTES, "mdi:download"], + "offpeak_download": ["Off Peak download", DATA_GIGABYTES, "mdi:download"], + "offpeak_upload": ["Off Peak Upload", DATA_GIGABYTES, "mdi:upload"], + "offpeak_total": ["Off Peak Total", DATA_GIGABYTES, "mdi:download"], + "onpeak_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"], } API_HA_MAP = ( diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 9a9250dbed6..659ef97d9de 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -1,13 +1,16 @@ """Constants for the Transmission Bittorent Client component.""" + +from homeassistant.const import DATA_RATE_MEGABYTES_PER_SECOND + DOMAIN = "transmission" SENSOR_TYPES = { "active_torrents": ["Active Torrents", "Torrents"], "current_status": ["Status", None], - "download_speed": ["Down Speed", "MB/s"], + "download_speed": ["Down Speed", DATA_RATE_MEGABYTES_PER_SECOND], "paused_torrents": ["Paused Torrents", "Torrents"], "total_torrents": ["Total Torrents", "Torrents"], - "upload_speed": ["Up Speed", "MB/s"], + "upload_speed": ["Up Speed", DATA_RATE_MEGABYTES_PER_SECOND], "completed_torrents": ["Completed Torrents", "Torrents"], "started_torrents": ["Started Torrents", "Torrents"], } diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index fec912f00d8..0bcdcf9d4c1 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, DATA_GIGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -21,7 +21,7 @@ DEFAULT_NAME = "Vultr {} {}" MONITORED_CONDITIONS = { ATTR_CURRENT_BANDWIDTH_USED: [ "Current Bandwidth Used", - "GB", + DATA_GIGABYTES, "mdi:chart-histogram", ], ATTR_PENDING_CHARGES: ["Pending Charges", "US$", "mdi:currency-usd"], diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index dcfdad963a7..94ee513f134 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -30,4 +30,3 @@ ATTR_UDP_PORT = "udp_port" # Units of measurement CURRENT_MA = "mA" -DATA_BYTES = "bytes" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index c3fc2d4e6c2..41e03d8c728 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -4,20 +4,13 @@ import logging from typing import Callable, List, Optional, Union from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import DATA_BYTES, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import WLED, WLEDDeviceEntity -from .const import ( - ATTR_LED_COUNT, - ATTR_MAX_POWER, - CURRENT_MA, - DATA_BYTES, - DATA_WLED_CLIENT, - DOMAIN, -) +from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_WLED_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/const.py b/homeassistant/const.py index e56ca49b389..2f97f6968de 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -378,6 +378,40 @@ MASS_POUNDS: str = "lb" # UV Index units UNIT_UV_INDEX: str = "UV index" +# Data units +DATA_BITS = "bit" +DATA_KILOBITS = "kbit" +DATA_MEGABITS = "Mbit" +DATA_GIGABITS = "Gbit" +DATA_BYTES = "B" +DATA_KILOBYTES = "kB" +DATA_MEGABYTES = "MB" +DATA_GIGABYTES = "GB" +DATA_TERABYTES = "TB" +DATA_PETABYTES = "PB" +DATA_EXABYTES = "EB" +DATA_ZETTABYTES = "ZB" +DATA_YOTTABYTES = "YB" +DATA_KIBIBYTES = "KiB" +DATA_MEBIBYTES = "MiB" +DATA_GIBIBYTES = "GiB" +DATA_TEBIBYTES = "TiB" +DATA_PEBIBYTES = "PiB" +DATA_EXBIBYTES = "EiB" +DATA_ZEBIBYTES = "ZiB" +DATA_YOBIBYTES = "YiB" +DATA_RATE_BITS_PER_SECOND = f"{DATA_BITS}/s" +DATA_RATE_KILOBITS_PER_SECOND = f"{DATA_KILOBITS}/s" +DATA_RATE_MEGABITS_PER_SECOND = f"{DATA_MEGABITS}/s" +DATA_RATE_GIGABITS_PER_SECOND = f"{DATA_GIGABITS}/s" +DATA_RATE_BYTES_PER_SECOND = f"{DATA_BYTES}/s" +DATA_RATE_KILOBYTES_PER_SECOND = f"{DATA_KILOBYTES}/s" +DATA_RATE_MEGABYTES_PER_SECOND = f"{DATA_MEGABYTES}/s" +DATA_RATE_GIGABYTES_PER_SECOND = f"{DATA_GIGABYTES}/s" +DATA_RATE_KIBIBYTES_PER_SECOND = f"{DATA_KIBIBYTES}/s" +DATA_RATE_MEBIBYTES_PER_SECOND = f"{DATA_MEBIBYTES}/s" +DATA_RATE_GIBIBYTES_PER_SECOND = f"{DATA_GIBIBYTES}/s" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" SERVICE_HOMEASSISTANT_RESTART = "restart" diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index 114daa7d2f7..c18476a92a9 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -4,6 +4,7 @@ import unittest import pytest import homeassistant.components.radarr.sensor as radarr +from homeassistant.const import DATA_GIGABYTES from tests.common import get_test_home_assistant @@ -218,7 +219,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": [], "monitored_conditions": ["diskspace"], } @@ -227,7 +228,7 @@ class TestRadarrSetup(unittest.TestCase): device.update() assert "263.10" == device.state assert "mdi:harddisk" == device.icon - assert "GB" == device.unit_of_measurement + assert DATA_GIGABYTES == device.unit_of_measurement assert "Radarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] @@ -238,7 +239,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["diskspace"], } @@ -247,7 +248,7 @@ class TestRadarrSetup(unittest.TestCase): device.update() assert "263.10" == device.state assert "mdi:harddisk" == device.icon - assert "GB" == device.unit_of_measurement + assert DATA_GIGABYTES == device.unit_of_measurement assert "Radarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] @@ -258,7 +259,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["commands"], } @@ -278,7 +279,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["movies"], } @@ -298,7 +299,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], } @@ -325,7 +326,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "1", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], } @@ -348,7 +349,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["status"], } @@ -368,7 +369,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "1", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], "ssl": "true", @@ -393,7 +394,7 @@ class TestRadarrSetup(unittest.TestCase): "platform": "radarr", "api_key": "foo", "days": "1", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], } diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 7edbfa065ad..7e03eb0fd41 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -10,6 +10,7 @@ import requests_mock import homeassistant.components.rest.sensor as rest import homeassistant.components.sensor as sensor +from homeassistant.const import DATA_MEGABYTES from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template from homeassistant.setup import setup_component @@ -125,7 +126,7 @@ class TestRestSensorSetup(unittest.TestCase): "method": "GET", "value_template": "{{ value_json.key }}", "name": "foo", - "unit_of_measurement": "MB", + "unit_of_measurement": DATA_MEGABYTES, "verify_ssl": "true", "timeout": 30, "authentication": "basic", @@ -153,7 +154,7 @@ class TestRestSensorSetup(unittest.TestCase): "value_template": "{{ value_json.key }}", "payload": '{ "device": "toaster"}', "name": "foo", - "unit_of_measurement": "MB", + "unit_of_measurement": DATA_MEGABYTES, "verify_ssl": "true", "timeout": 30, "authentication": "basic", @@ -181,7 +182,7 @@ class TestRestSensor(unittest.TestCase): ), ) self.name = "foo" - self.unit_of_measurement = "MB" + self.unit_of_measurement = DATA_MEGABYTES self.device_class = None self.value_template = template("{{ value_json.key }}") self.value_template.hass = self.hass diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 38382dc70ab..300d201079b 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -6,6 +6,7 @@ import unittest import pytest import homeassistant.components.sonarr.sensor as sonarr +from homeassistant.const import DATA_GIGABYTES from tests.common import get_test_home_assistant @@ -497,7 +498,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": [], "monitored_conditions": ["diskspace"], } @@ -506,7 +507,7 @@ class TestSonarrSetup(unittest.TestCase): device.update() assert "263.10" == device.state assert "mdi:harddisk" == device.icon - assert "GB" == device.unit_of_measurement + assert DATA_GIGABYTES == device.unit_of_measurement assert "Sonarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] @@ -517,7 +518,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["diskspace"], } @@ -526,7 +527,7 @@ class TestSonarrSetup(unittest.TestCase): device.update() assert "263.10" == device.state assert "mdi:harddisk" == device.icon - assert "GB" == device.unit_of_measurement + assert DATA_GIGABYTES == device.unit_of_measurement assert "Sonarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] @@ -537,7 +538,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["commands"], } @@ -557,7 +558,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["queue"], } @@ -577,7 +578,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["series"], } @@ -599,7 +600,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["wanted"], } @@ -621,7 +622,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], } @@ -645,7 +646,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "1", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], } @@ -665,7 +666,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "2", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["status"], } @@ -685,7 +686,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "1", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], "ssl": "true", @@ -707,7 +708,7 @@ class TestSonarrSetup(unittest.TestCase): "platform": "sonarr", "api_key": "foo", "days": "1", - "unit": "GB", + "unit": DATA_GIGABYTES, "include_paths": ["/data"], "monitored_conditions": ["upcoming"], } diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index eac75a3b4e7..82748c122ab 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the Start.ca sensor platform.""" from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData +from homeassistant.const import DATA_GIGABYTES from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -55,47 +56,47 @@ async def test_capped_setup(hass, aioclient_mock): assert state.state == "76.24" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "400" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "95.05" @@ -150,47 +151,47 @@ async def test_unlimited_setup(hass, aioclient_mock): assert state.state == "0" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "inf" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "inf" diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index 30bb98911f8..641112e6362 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the TekSavvy sensor platform.""" from homeassistant.bootstrap import async_setup_component from homeassistant.components.teksavvy.sensor import TekSavvyData +from homeassistant.const import DATA_GIGABYTES from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -45,31 +46,31 @@ async def test_capped_setup(hass, aioclient_mock): await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.teksavvy_data_limit") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "400" state = hass.states.get("sensor.teksavvy_off_peak_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "36.24" state = hass.states.get("sensor.teksavvy_off_peak_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "1.58" state = hass.states.get("sensor.teksavvy_off_peak_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "37.82" state = hass.states.get("sensor.teksavvy_on_peak_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_on_peak_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "8.82" state = hass.states.get("sensor.teksavvy_on_peak_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "235.57" state = hass.states.get("sensor.teksavvy_usage_ratio") @@ -77,11 +78,11 @@ async def test_capped_setup(hass, aioclient_mock): assert state.state == "56.69" state = hass.states.get("sensor.teksavvy_usage") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_remaining") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "173.25" @@ -126,35 +127,35 @@ async def test_unlimited_setup(hass, aioclient_mock): await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.teksavvy_data_limit") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "inf" state = hass.states.get("sensor.teksavvy_off_peak_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "36.24" state = hass.states.get("sensor.teksavvy_off_peak_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "1.58" state = hass.states.get("sensor.teksavvy_off_peak_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "37.82" state = hass.states.get("sensor.teksavvy_on_peak_download") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_on_peak_upload") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "8.82" state = hass.states.get("sensor.teksavvy_on_peak_total") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "235.57" state = hass.states.get("sensor.teksavvy_usage") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_usage_ratio") @@ -162,7 +163,7 @@ async def test_unlimited_setup(hass, aioclient_mock): assert state.state == "0" state = hass.states.get("sensor.teksavvy_remaining") - assert state.attributes.get("unit_of_measurement") == "GB" + assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES assert state.state == "inf" diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 4da60783c44..80fd05a41cc 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -10,7 +10,12 @@ import voluptuous as vol from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import CONF_SUBSCRIPTION import homeassistant.components.vultr.sensor as vultr -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PLATFORM +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_PLATFORM, + DATA_GIGABYTES, +) from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG @@ -83,7 +88,7 @@ class TestVultrSensorSetup(unittest.TestCase): device.update() - if device.unit_of_measurement == "GB": # Test Bandwidth Used + if device.unit_of_measurement == DATA_GIGABYTES: # Test Bandwidth Used if device.subscription == "576965": assert "Vultr my new server Current Bandwidth Used" == device.name assert "mdi:chart-histogram" == device.icon diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 779e39c67ce..894968f5db4 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -8,10 +8,9 @@ from homeassistant.components.wled.const import ( ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, - DATA_BYTES, DOMAIN, ) -from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util From 8d1f8055dd8b88171629f887cfc807244896dd7a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Feb 2020 19:20:26 +0100 Subject: [PATCH 232/378] Fix spelling of NETGEAR and CalDAV in manifests (#31790) --- homeassistant/components/caldav/manifest.json | 2 +- homeassistant/components/netgear/manifest.json | 2 +- homeassistant/components/netgear_lte/manifest.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 2f48fb5fc27..85dc005a6a8 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -1,6 +1,6 @@ { "domain": "caldav", - "name": "CalDav", + "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": ["caldav==0.6.1"], "dependencies": [], diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 4cfffab7d73..c5685411045 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -1,6 +1,6 @@ { "domain": "netgear", - "name": "Netgear", + "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": ["pynetgear==0.6.1"], "dependencies": [], diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 0be2ca146b1..43cf6e34480 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,6 +1,6 @@ { "domain": "netgear_lte", - "name": "Netgear LTE", + "name": "NETGEAR LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": ["eternalegypt==0.0.11"], "dependencies": [], From 9e7185c676a19906248d46d8ea3ffb65fa23a3f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Feb 2020 10:22:06 -0800 Subject: [PATCH 233/378] Write state if schedule update state from async context (#31758) * Write state if schedule update state from async context * Fix most tests * Fix test and PS4 I/O in event loop * Fix ps4 better --- .../homematicip_cloud/alarm_control_panel.py | 2 +- homeassistant/components/ps4/media_player.py | 13 +++---------- homeassistant/helpers/entity.py | 5 ++++- .../device_sun_light_trigger/test_init.py | 1 + tests/components/rflink/test_binary_sensor.py | 4 ++++ tests/components/rflink/test_cover.py | 1 + tests/components/rflink/test_light.py | 1 + tests/components/rflink/test_switch.py | 1 + 8 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index c2f4d833a35..902755bfa07 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -98,7 +98,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) - self.async_schedule_update_ha_state() + self.async_schedule_update_ha_state(True) @property def name(self) -> str: diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 33b5c556c7d..28d201d78cd 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -97,11 +97,7 @@ class PS4Device(MediaPlayerDevice): def status_callback(self): """Handle status callback. Parse status.""" self._parse_status() - - @callback - def schedule_update(self): - """Schedules update with HA.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def subscribe_to_protocol(self): @@ -184,7 +180,6 @@ class PS4Device(MediaPlayerDevice): self._media_content_id = title_id if self._use_saved(): _LOGGER.debug("Using saved data for media: %s", title_id) - self.schedule_update() return self._media_title = name @@ -223,13 +218,11 @@ class PS4Device(MediaPlayerDevice): """Set states for state idle.""" self.reset_title() self._state = STATE_IDLE - self.schedule_update() def state_standby(self): """Set states for state standby.""" self.reset_title() self._state = STATE_STANDBY - self.schedule_update() def state_unknown(self): """Set states for state unknown.""" @@ -286,8 +279,8 @@ class PS4Device(MediaPlayerDevice): self._media_image = art or None self._media_type = media_type - self.update_list() - self.schedule_update() + await self.hass.async_add_executor_job(self.update_list) + self.async_write_ha_state() def update_list(self): """Update Game List, Correct data if different.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4c3b9448f5a..49ed0f4a567 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -441,7 +441,10 @@ class Entity(ABC): If state is changed more than once before the ha state change task has been executed, the intermediate state transitions will be missed. """ - self.hass.async_create_task(self.async_update_ha_state(force_refresh)) + if force_refresh: + self.hass.async_create_task(self.async_update_ha_state(force_refresh)) + else: + self.async_write_ha_state() async def async_device_update(self, warning=True): """Process 'update' or 'async_update' from entity. diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index dda4a90f31b..c8d0e334412 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -187,6 +187,7 @@ async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanne # person home switches on hass.states.async_set(device_1, STATE_HOME) await hass.async_block_till_done() + await hass.async_block_till_done() assert all( light.is_on(hass, ent_id) diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 18c4f946318..2a67cf5348d 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -130,6 +130,7 @@ async def test_off_delay(hass, monkeypatch): async_fire_time_changed(hass, future) event_callback(on_event) await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") assert state.state == STATE_ON assert len(events) == 1 @@ -140,6 +141,7 @@ async def test_off_delay(hass, monkeypatch): async_fire_time_changed(hass, future) event_callback(on_event) await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") assert state.state == STATE_ON assert len(events) == 2 @@ -149,6 +151,7 @@ async def test_off_delay(hass, monkeypatch): with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") assert state.state == STATE_ON assert len(events) == 2 @@ -158,6 +161,7 @@ async def test_off_delay(hass, monkeypatch): with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") assert state.state == STATE_OFF assert len(events) == 3 diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index dc286502068..e10cdc20143 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -144,6 +144,7 @@ async def test_firing_bus_event(hass, monkeypatch): # test event for new unconfigured sensor event_callback({"id": "protocol_0_0", "command": "down"}) await hass.async_block_till_done() + await hass.async_block_till_done() assert calls[0].data == {"state": "down", "entity_id": DOMAIN + ".test"} diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index 5dc06b5b2ff..87696191ac8 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -184,6 +184,7 @@ async def test_firing_bus_event(hass, monkeypatch): # test event for new unconfigured sensor event_callback({"id": "protocol_0_0", "command": "off"}) await hass.async_block_till_done() + await hass.async_block_till_done() assert calls[0].data == {"state": "off", "entity_id": DOMAIN + ".test"} diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index d1fced33208..bcade409d3e 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -214,6 +214,7 @@ async def test_device_defaults(hass, monkeypatch): # test event for new unconfigured sensor event_callback({"id": "protocol_0_0", "command": "off"}) await hass.async_block_till_done() + await hass.async_block_till_done() assert calls[0].data == {"state": "off", "entity_id": DOMAIN + ".test"} From f091e0412fe25d3e84305454cc3ce3380645d5d7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 13 Feb 2020 12:30:38 -0700 Subject: [PATCH 234/378] Add support for real-time data from SimpliSafe (#31424) * Add support for real-time data from SimpliSafe * Updated requirements * Linting * Ensure dispatcher topic contains the domain * Don't bother with a partial * Websovket dataclass and other code review * Ensure initial_event_to_use works with error * Don't inline methods * Don't abuse loop variable * Simplify initial event retrieval * Add connection lost and restored events * Revert "Add connection lost and restored events" This reverts commit e7ffe05938e6cd13a5426f8a605260056fa04de0. * Make _on_disconnect a static method * Code review comments * Allow entities to opt out of REST and/or websocket API updates * Revert "Allow entities to opt out of REST and/or websocket API updates" This reverts commit 1989f2e00e0b95dd466bcc803e7c83afab6d2763. * Code review comments * Fix issues with events not triggering correct entities * Bug fixes --- .../components/simplisafe/__init__.py | 289 ++++++++++++++++-- .../simplisafe/alarm_control_panel.py | 221 ++++++++------ homeassistant/components/simplisafe/const.py | 20 +- homeassistant/components/simplisafe/lock.py | 52 +++- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 451 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 7cd1fd1bb2d..74c7c3fd079 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,10 +1,18 @@ """Support for SimpliSafe alarm systems.""" import asyncio +from dataclasses import dataclass, field +from datetime import datetime import logging +from typing import Optional from simplipy import API -from simplipy.errors import InvalidCredentialsError, SimplipyError -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +from simplipy.entity import EntityTypes +from simplipy.errors import InvalidCredentialsError, SimplipyError, WebsocketError +from simplipy.websocket import ( + EVENT_LOCK_LOCKED, + EVENT_LOCK_UNLOCKED, + get_event_type_from_payload, +) import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -21,36 +29,50 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, ) +from homeassistant.util.dt import utc_from_timestamp from .config_flow import configured_instances -from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE +from .const import ( + ATTR_ALARM_DURATION, + ATTR_ALARM_VOLUME, + ATTR_CHIME_VOLUME, + ATTR_ENTRY_DELAY_AWAY, + ATTR_ENTRY_DELAY_HOME, + ATTR_EXIT_DELAY_AWAY, + ATTR_EXIT_DELAY_HOME, + ATTR_LIGHT, + ATTR_VOICE_PROMPT_VOLUME, + DATA_CLIENT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + VOLUMES, +) _LOGGER = logging.getLogger(__name__) CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" +TOPIC_UPDATE = "simplisafe_update_data_{0}" -ATTR_ALARM_DURATION = "alarm_duration" -ATTR_ALARM_VOLUME = "alarm_volume" -ATTR_CHIME_VOLUME = "chime_volume" -ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" -ATTR_ENTRY_DELAY_HOME = "entry_delay_home" -ATTR_EXIT_DELAY_AWAY = "exit_delay_away" -ATTR_EXIT_DELAY_HOME = "exit_delay_home" -ATTR_LIGHT = "light" +DEFAULT_SOCKET_MIN_RETRY = 15 +DEFAULT_WATCHDOG_SECONDS = 5 * 60 + +WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] + +ATTR_LAST_EVENT_INFO = "last_event_info" +ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" +ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" +ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" ATTR_PIN_VALUE = "pin" ATTR_SYSTEM_ID = "system_id" -ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" - -VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -283,8 +305,133 @@ async def async_unload_entry(hass, entry): return True +@dataclass(frozen=True) +class SimpliSafeWebsocketEvent: + """Define a representation of a parsed websocket event.""" + + event_data: dict + + changed_by: Optional[str] = field(init=False) + event_type: Optional[str] = field(init=False) + info: str = field(init=False) + sensor_name: str = field(init=False) + sensor_serial: str = field(init=False) + sensor_type: EntityTypes = field(init=False) + system_id: int = field(init=False) + timestamp: datetime = field(init=False) + + def __post_init__(self): + """Initialize.""" + object.__setattr__(self, "changed_by", self.event_data["pinName"]) + object.__setattr__( + self, "event_type", get_event_type_from_payload(self.event_data) + ) + object.__setattr__(self, "info", self.event_data["info"]) + object.__setattr__(self, "sensor_name", self.event_data["sensorName"]) + object.__setattr__(self, "sensor_serial", self.event_data["sensorSerial"]) + try: + object.__setattr__( + self, "sensor_type", EntityTypes(self.event_data["sensorType"]).name + ) + except ValueError: + _LOGGER.warning( + 'Encountered unknown entity type: %s ("%s"). Please report it at' + "https://github.com/home-assistant/home-assistant/issues.", + self.event_data["sensorType"], + self.event_data["sensorName"], + ) + object.__setattr__(self, "sensor_type", None) + object.__setattr__(self, "system_id", self.event_data["sid"]) + object.__setattr__( + self, "timestamp", utc_from_timestamp(self.event_data["eventTimestamp"]) + ) + + +class SimpliSafeWebsocket: + """Define a SimpliSafe websocket "manager" object.""" + + def __init__(self, hass, websocket): + """Initialize.""" + self._hass = hass + self._websocket = websocket + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self._websocket_reconnect_underway = False + self._websocket_watchdog_listener = None + self.last_events = {} + + async def _async_attempt_websocket_connect(self): + """Attempt to connect to the websocket (retrying later on fail).""" + self._websocket_reconnect_underway = True + + try: + await self._websocket.async_connect() + except WebsocketError as err: + _LOGGER.error("Error with the websocket connection: %s", err) + self._websocket_reconnect_delay = min( + 2 * self._websocket_reconnect_delay, 480 + ) + async_call_later( + self._hass, + self._websocket_reconnect_delay, + self.async_websocket_connect, + ) + else: + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self._websocket_reconnect_underway = False + + async def _async_websocket_reconnect(self, event_time): + """Forcibly disconnect from and reconnect to the websocket.""" + _LOGGER.debug("Websocket watchdog expired; forcing socket reconnection") + await self.async_websocket_disconnect() + await self._async_attempt_websocket_connect() + + def _on_connect(self): + """Define a handler to fire when the websocket is connected.""" + _LOGGER.info("Connected to websocket") + _LOGGER.debug("Websocket watchdog starting") + if self._websocket_watchdog_listener is not None: + self._websocket_watchdog_listener() + self._websocket_watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, self._async_websocket_reconnect + ) + + @staticmethod + def _on_disconnect(): + """Define a handler to fire when the websocket is disconnected.""" + _LOGGER.info("Disconnected from websocket") + + def _on_event(self, data): + """Define a handler to fire when a new SimpliSafe event arrives.""" + event = SimpliSafeWebsocketEvent(data) + _LOGGER.debug("New websocket event: %s", event) + self.last_events[data["sid"]] = event + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(data["sid"])) + + _LOGGER.debug("Resetting websocket watchdog") + self._websocket_watchdog_listener() + self._websocket_watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, self._async_websocket_reconnect + ) + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + + async def async_websocket_connect(self): + """Register handlers and connect to the websocket.""" + if self._websocket_reconnect_underway: + return + + self._websocket.on_connect(self._on_connect) + self._websocket.on_disconnect(self._on_disconnect) + self._websocket.on_event(self._on_event) + + await self._async_attempt_websocket_connect() + + async def async_websocket_disconnect(self): + """Disconnect from the websocket.""" + await self._websocket.async_disconnect() + + class SimpliSafe: - """Define a SimpliSafe API object.""" + """Define a SimpliSafe data object.""" def __init__(self, hass, api, config_entry): """Initialize.""" @@ -292,14 +439,15 @@ class SimpliSafe: self._config_entry = config_entry self._emergency_refresh_token_used = False self._hass = hass - self.last_event_data = {} + self.initial_event_to_use = {} self.systems = None + self.websocket = SimpliSafeWebsocket(hass, api.websocket) async def async_init(self): """Initialize the data class.""" - self.systems = await self._api.get_systems() + asyncio.create_task(self.websocket.async_websocket_connect()) - # Register the base station for each system: + self.systems = await self._api.get_systems() for system in self.systems.values(): self._hass.async_create_task( async_register_base_station( @@ -307,6 +455,17 @@ class SimpliSafe: ) ) + # Future events will come from the websocket, but since subscription to the + # websocket doesn't provide the most recent event, we grab it from the REST + # API to ensure event-related attributes aren't empty on startup: + try: + self.initial_event_to_use[ + system.system_id + ] = await system.get_latest_event() + except SimplipyError as err: + _LOGGER.error("Error while fetching initial event: %s", err) + self.initial_event_to_use[system.system_id] = {} + async def refresh(event_time): """Refresh data from the SimpliSafe account.""" await self.async_update() @@ -323,7 +482,8 @@ class SimpliSafe: async def update_system(system): """Update a system.""" await system.update() - self.last_event_data[system.system_id] = await system.get_latest_event() + _LOGGER.debug('Updated REST API data for "%s"', system.address) + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id)) tasks = [update_system(system) for system in self.systems.values()] @@ -371,26 +531,41 @@ class SimpliSafe: if self._emergency_refresh_token_used: self._emergency_refresh_token_used = False - _LOGGER.debug("Updated data for all SimpliSafe systems") - async_dispatcher_send(self._hass, TOPIC_UPDATE) - class SimpliSafeEntity(Entity): """Define a base SimpliSafe entity.""" - def __init__(self, system, name, *, serial=None): + def __init__(self, simplisafe, system, name, *, serial=None): """Initialize.""" self._async_unsub_dispatcher_connect = None - self._attrs = {ATTR_SYSTEM_ID: system.system_id} + self._last_processed_websocket_event = None self._name = name self._online = True + self._simplisafe = simplisafe self._system = system + self.websocket_events_to_listen_for = [] if serial: self._serial = serial else: self._serial = system.serial + self._attrs = { + ATTR_LAST_EVENT_INFO: simplisafe.initial_event_to_use[system.system_id].get( + "info" + ), + ATTR_LAST_EVENT_SENSOR_NAME: simplisafe.initial_event_to_use[ + system.system_id + ].get("sensorName"), + ATTR_LAST_EVENT_SENSOR_TYPE: simplisafe.initial_event_to_use[ + system.system_id + ].get("sensorType"), + ATTR_LAST_EVENT_TIMESTAMP: simplisafe.initial_event_to_use[ + system.system_id + ].get("eventTimestamp"), + ATTR_SYSTEM_ID: system.system_id, + } + @property def available(self): """Return whether the entity is available.""" @@ -427,6 +602,36 @@ class SimpliSafeEntity(Entity): """Return the unique ID of the entity.""" return self._serial + @callback + def _async_should_ignore_websocket_event(self, event): + """Return whether this entity should ignore a particular websocket event. + + Note that we can't check for a final condition – whether the event belongs to + a particular entity, like a lock – because some events (like arming the system + from a keypad _or_ from the website) should impact the same entity. + """ + # We've already processed this event: + if self._last_processed_websocket_event == event: + return True + + # This is an event for a system other than the one this entity belongs to: + if event.system_id != self._system.system_id: + return True + + # This isn't an event that this entity cares about: + if event.event_type not in self.websocket_events_to_listen_for: + return True + + # This event is targeted at a specific entity whose serial number is different + # from this one's: + if ( + event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL + and event.sensor_serial != self._serial + ): + return True + + return False + async def async_added_to_hass(self): """Register callbacks.""" @@ -436,9 +641,41 @@ class SimpliSafeEntity(Entity): self.async_schedule_update_ha_state(True) self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.hass, TOPIC_UPDATE.format(self._system.system_id), update ) + async def async_update(self): + """Update the entity.""" + self.async_update_from_rest_api() + + last_websocket_event = self._simplisafe.websocket.last_events.get( + self._system.system_id + ) + + if self._async_should_ignore_websocket_event(last_websocket_event): + return + + self._last_processed_websocket_event = last_websocket_event + self._attrs.update( + { + ATTR_LAST_EVENT_INFO: last_websocket_event.info, + ATTR_LAST_EVENT_SENSOR_NAME: last_websocket_event.sensor_name, + ATTR_LAST_EVENT_SENSOR_TYPE: last_websocket_event.sensor_type, + ATTR_LAST_EVENT_TIMESTAMP: last_websocket_event.timestamp, + } + ) + self.async_update_from_websocket_event(last_websocket_event) + + @callback + def async_update_from_rest_api(self): + """Update the entity with the provided REST API data.""" + pass + + @callback + def async_update_from_websocket_event(self, event): + """Update the entity with the provided websocket API data.""" + pass + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 362c0244749..c675f9c2748 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -2,9 +2,21 @@ import logging import re -from simplipy.entity import EntityTypes +from simplipy.errors import SimplipyError from simplipy.system import SystemStates -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +from simplipy.websocket import ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, +) from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -23,40 +35,33 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.core import callback from . import SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN +from .const import ( + ATTR_ALARM_DURATION, + ATTR_ALARM_VOLUME, + ATTR_CHIME_VOLUME, + ATTR_ENTRY_DELAY_AWAY, + ATTR_ENTRY_DELAY_HOME, + ATTR_EXIT_DELAY_AWAY, + ATTR_EXIT_DELAY_HOME, + ATTR_LIGHT, + ATTR_VOICE_PROMPT_VOLUME, + DATA_CLIENT, + DOMAIN, + VOLUME_STRING_MAP, +) _LOGGER = logging.getLogger(__name__) -ATTR_ALARM_DURATION = "alarm_duration" -ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" -ATTR_CHIME_VOLUME = "chime_volume" -ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" -ATTR_ENTRY_DELAY_HOME = "entry_delay_home" -ATTR_EXIT_DELAY_AWAY = "exit_delay_away" -ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_GSM_STRENGTH = "gsm_strength" -ATTR_LAST_EVENT_INFO = "last_event_info" -ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" -ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" -ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" -ATTR_LAST_EVENT_TYPE = "last_event_type" -ATTR_LIGHT = "light" +ATTR_PIN_NAME = "pin_name" ATTR_RF_JAMMING = "rf_jamming" -ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -VOLUME_STRING_MAP = { - VOLUME_HIGH: "high", - VOLUME_LOW: "low", - VOLUME_MEDIUM: "medium", - VOLUME_OFF: "off", -} - async def async_setup_entry(hass, entry, async_add_entities): """Set up a SimpliSafe alarm control panel based on a config entry.""" @@ -75,33 +80,42 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): def __init__(self, simplisafe, system, code): """Initialize the SimpliSafe alarm.""" - super().__init__(system, "Alarm Control Panel") + super().__init__(simplisafe, system, "Alarm Control Panel") self._changed_by = None self._code = code - self._simplisafe = simplisafe - self._state = None + self._last_event = None - if self._system.version == 3: - self._attrs.update( - { - ATTR_ALARM_DURATION: self._system.alarm_duration, - ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], - ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, - ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], - ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, - ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, - ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, - ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, - ATTR_GSM_STRENGTH: self._system.gsm_strength, - ATTR_LIGHT: self._system.light, - ATTR_RF_JAMMING: self._system.rf_jamming, - ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ - self._system.voice_prompt_volume - ], - ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, - ATTR_WIFI_STRENGTH: self._system.wifi_strength, - } - ) + if system.alarm_going_off: + self._state = STATE_ALARM_TRIGGERED + elif system.state == SystemStates.away: + self._state = STATE_ALARM_ARMED_AWAY + elif system.state in ( + SystemStates.away_count, + SystemStates.exit_delay, + SystemStates.home_count, + ): + self._state = STATE_ALARM_ARMING + elif system.state == SystemStates.home: + self._state = STATE_ALARM_ARMED_HOME + elif system.state == SystemStates.off: + self._state = STATE_ALARM_DISARMED + else: + self._state = None + + for event_type in ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, + ): + self.websocket_events_to_listen_for.append(event_type) @property def changed_by(self): @@ -139,71 +153,96 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): if not self._validate_code(code, "disarming"): return - await self._system.set_off() + try: + await self._system.set_off() + except SimplipyError as err: + _LOGGER.error('Error while disarming "%s": %s', self._system.name, err) + return + + self._state = STATE_ALARM_DISARMED async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if not self._validate_code(code, "arming home"): return - await self._system.set_home() + try: + await self._system.set_home() + except SimplipyError as err: + _LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err) + return + + self._state = STATE_ALARM_ARMED_HOME async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if not self._validate_code(code, "arming away"): return - await self._system.set_away() + try: + await self._system.set_away() + except SimplipyError as err: + _LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err) + return - async def async_update(self): - """Update alarm status.""" - last_event = self._simplisafe.last_event_data[self._system.system_id] - - if last_event.get("pinName"): - self._changed_by = last_event["pinName"] + self._state = STATE_ALARM_ARMING + @callback + def async_update_from_rest_api(self): + """Update the entity with the provided REST API data.""" if self._system.state == SystemStates.error: self._online = False return - self._online = True - if self._system.alarm_going_off: + if self._system.version == 3: + self._attrs.update( + { + ATTR_ALARM_DURATION: self._system.alarm_duration, + ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], + ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, + ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], + ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, + ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, + ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, + ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, + ATTR_GSM_STRENGTH: self._system.gsm_strength, + ATTR_LIGHT: self._system.light, + ATTR_RF_JAMMING: self._system.rf_jamming, + ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ + self._system.voice_prompt_volume + ], + ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, + ATTR_WIFI_STRENGTH: self._system.wifi_strength, + } + ) + + @callback + def async_update_from_websocket_event(self, event): + """Update the entity with the provided websocket API event data.""" + if event.event_type in ( + EVENT_ALARM_CANCELED, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + ): + self._state = STATE_ALARM_DISARMED + elif event.event_type == EVENT_ALARM_TRIGGERED: self._state = STATE_ALARM_TRIGGERED - elif self._system.state == SystemStates.away: + elif event.event_type in ( + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + ): self._state = STATE_ALARM_ARMED_AWAY - elif self._system.state in ( - SystemStates.away_count, - SystemStates.exit_delay, - SystemStates.home_count, + elif event.event_type == EVENT_ARMED_HOME: + self._state = STATE_ALARM_ARMED_HOME + elif event.event_type in ( + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, ): self._state = STATE_ALARM_ARMING - elif self._system.state == SystemStates.home: - self._state = STATE_ALARM_ARMED_HOME - elif self._system.state == SystemStates.off: - self._state = STATE_ALARM_DISARMED else: self._state = None - try: - last_event_sensor_type = EntityTypes(last_event["sensorType"]).name - except ValueError: - _LOGGER.warning( - 'Encountered unknown entity type: %s ("%s"). Please report it at' - "https://github.com/home-assistant/home-assistant/issues.", - last_event["sensorType"], - last_event["sensorName"], - ) - last_event_sensor_type = None - - self._attrs.update( - { - ATTR_LAST_EVENT_INFO: last_event["info"], - ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type, - ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( - last_event["eventTimestamp"] - ), - ATTR_LAST_EVENT_TYPE: last_event["eventType"], - } - ) + self._changed_by = event.changed_by diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index 4dfef39de46..6ca5f8323a7 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -1,10 +1,28 @@ """Define constants for the SimpliSafe component.""" from datetime import timedelta +from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF + DOMAIN = "simplisafe" DATA_CLIENT = "client" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) -TOPIC_UPDATE = "update" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" +ATTR_LIGHT = "light" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" + +VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] +VOLUME_STRING_MAP = { + VOLUME_HIGH: "high", + VOLUME_LOW: "low", + VOLUME_MEDIUM: "medium", + VOLUME_OFF: "off", +} diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 10c5d310e73..58448ec4599 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,10 +1,12 @@ """Support for SimpliSafe locks.""" import logging +from simplipy.errors import SimplipyError from simplipy.lock import LockStates +from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED from homeassistant.components.lock import LockDevice -from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED +from homeassistant.core import callback from . import SimpliSafeEntity from .const import DATA_CLIENT, DOMAIN @@ -15,19 +17,13 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_JAMMED = "jammed" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" -STATE_MAP = { - LockStates.locked: STATE_LOCKED, - LockStates.unknown: STATE_UNKNOWN, - LockStates.unlocked: STATE_UNLOCKED, -} - async def async_setup_entry(hass, entry, async_add_entities): """Set up SimpliSafe locks based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] async_add_entities( [ - SimpliSafeLock(system, lock) + SimpliSafeLock(simplisafe, system, lock) for system in simplisafe.systems.values() for lock in system.locks.values() ] @@ -37,32 +33,48 @@ async def async_setup_entry(hass, entry, async_add_entities): class SimpliSafeLock(SimpliSafeEntity, LockDevice): """Define a SimpliSafe lock.""" - def __init__(self, system, lock): + def __init__(self, simplisafe, system, lock): """Initialize.""" - super().__init__(system, lock.name, serial=lock.serial) + super().__init__(simplisafe, system, lock.name, serial=lock.serial) + self._is_locked = False self._lock = lock + for event_type in (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED): + self.websocket_events_to_listen_for.append(event_type) + @property def is_locked(self): """Return true if the lock is locked.""" - return STATE_MAP.get(self._lock.state) == STATE_LOCKED + return self._is_locked async def async_lock(self, **kwargs): """Lock the lock.""" - await self._lock.lock() + try: + await self._lock.lock() + except SimplipyError as err: + _LOGGER.error('Error while locking "%s": %s', self._lock.name, err) + return + + self._is_locked = True async def async_unlock(self, **kwargs): """Unlock the lock.""" - await self._lock.unlock() + try: + await self._lock.unlock() + except SimplipyError as err: + _LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) + return - async def async_update(self): - """Update lock status.""" + self._is_locked = False + + @callback + def async_update_from_rest_api(self): + """Update the entity with the provided REST API data.""" if self._lock.offline or self._lock.disabled: self._online = False return self._online = True - self._attrs.update( { ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, @@ -70,3 +82,11 @@ class SimpliSafeLock(SimpliSafeEntity, LockDevice): ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery, } ) + + @callback + def async_update_from_websocket_event(self, event): + """Update the entity with the provided websocket event data.""" + if event.event_type == EVENT_LOCK_LOCKED: + self._is_locked = True + else: + self._is_locked = False diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index f95db72d45a..3b04d26732c 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==6.1.0"], + "requirements": ["simplisafe-python==7.1.0"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index ac811f0889f..fd11709ec5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1829,7 +1829,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==6.1.0 +simplisafe-python==7.1.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 130f9c4530d..37fa61b323c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -622,7 +622,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==6.1.0 +simplisafe-python==7.1.0 # homeassistant.components.sleepiq sleepyq==0.7 From fdfe88566bfc6d6e284c4ddaf26aa12383d394f7 Mon Sep 17 00:00:00 2001 From: MrDadoo Date: Thu, 13 Feb 2020 21:56:20 +0100 Subject: [PATCH 235/378] Update onewire component (#31419) * Added some measurement points for Family 26. Added three attriubtes for each sensor, DEVICE_FILE, RAW_VALUE and SENSOR_TYPE Added some comments. Updated to get config values for owserver to get owport and owhost. * Changed to _LOGGER.debug in some places Resorted includes * Fixup of code to reflect review comment, comply to Black and pass tests * Added unique_id Entity property with device file id and added device_file as attribute that takes its data from Entities unique_id-. * Missing blank line identified by fake8 and black * Changed to let device_state_attributes return attribute device_file from the self._device_file member variable of the entity. * Changed from info to debug logging --- homeassistant/components/onewire/sensor.py | 36 +++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 936bf9f751b..6a7f282ac87 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -19,6 +19,7 @@ CONF_NAMES = "names" DEFAULT_MOUNT_DIR = "/sys/bus/w1/devices/" DEVICE_SENSORS = { + # Family : { SensorType: owfs path } "10": {"temperature": "temperature"}, "12": {"temperature": "TAI8570/temperature", "pressure": "TAI8570/pressure"}, "22": {"temperature": "temperature"}, @@ -27,6 +28,9 @@ DEVICE_SENSORS = { "humidity": "humidity", "pressure": "B1-R1-A/pressure", "illuminance": "S3-R1-A/illuminance", + "voltage_VAD": "VAD", + "voltage_VDD": "VDD", + "current": "IAD", }, "28": {"temperature": "temperature"}, "3B": {"temperature": "temperature"}, @@ -54,6 +58,7 @@ HOBBYBOARD_EF = { } SENSOR_TYPES = { + # SensorType: [ Measured unit, Unit ] "temperature": ["temperature", TEMP_CELSIUS], "humidity": ["humidity", "%"], "humidity_raw": ["humidity", "%"], @@ -70,6 +75,10 @@ SENSOR_TYPES = { "counter_a": ["counter", "count"], "counter_b": ["counter", "count"], "HobbyBoard": ["none", "none"], + "voltage": ["voltage", "V"], + "voltage_VAD": ["voltage", "V"], + "voltage_VDD": ["voltage", "V"], + "current": ["current", "A"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -95,11 +104,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): base_dir = config[CONF_MOUNT_DIR] owport = config[CONF_PORT] owhost = config.get(CONF_HOST) + if owhost: + _LOGGER.debug("Initializing using %s:%s", owhost, owport) + else: + _LOGGER.debug("Initializing using %s", base_dir) + devs = [] device_names = {} - if "names" in config: - if isinstance(config["names"], dict): - device_names = config["names"] + if CONF_NAMES in config: + if isinstance(config[CONF_NAMES], dict): + device_names = config[CONF_NAMES] # We have an owserver on a remote(or local) host/port if owhost: @@ -112,7 +126,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) devices = [] for device in devices: - _LOGGER.debug("found device: %s", device) + _LOGGER.debug("Found device: %s", device) family = owproxy.read(f"{device}family").decode() dev_type = "std" if "EF" in family: @@ -200,6 +214,7 @@ class OneWire(Entity): self._device_file = device_file self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._state = None + self._value_raw = None def _read_value_raw(self): """Read the value as it is returned by the sensor.""" @@ -224,6 +239,16 @@ class OneWire(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return {"device_file": self._device_file, "raw_value": self._value_raw} + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device_file + class OneWireProxy(OneWire): """Implementation of a One wire Sensor through owserver.""" @@ -249,6 +274,7 @@ class OneWireProxy(OneWire): _LOGGER.error("Owserver failure in read(), got: %s", exc) if value_read: value = round(float(value_read), 1) + self._value_raw = float(value_read) self._state = value @@ -267,6 +293,7 @@ class OneWireDirect(OneWire): if equals_pos != -1: value_string = lines[1][equals_pos + 2 :] value = round(float(value_string) / 1000.0, 1) + self._value_raw = float(value_string) self._state = value @@ -280,6 +307,7 @@ class OneWireOWFS(OneWire): value_read = self._read_value_raw() if len(value_read) == 1: value = round(float(value_read[0]), 1) + self._value_raw = float(value_read[0]) except ValueError: _LOGGER.warning("Invalid value read from %s", self._device_file) except FileNotFoundError: From 6211a2bb987fcac4d0257b38bf9f4248f8526bde Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 13 Feb 2020 22:12:09 +0100 Subject: [PATCH 236/378] Add multi select support to config validation and to custom serializer (#31798) --- homeassistant/helpers/config_validation.py | 20 ++++++++++++++++++++ tests/helpers/test_config_validation.py | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f1caf38bf8b..0fc9dbbaae1 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -588,6 +588,23 @@ def ensure_list_csv(value: Any) -> List: return ensure_list(value) +def multi_select(options: dict) -> Callable[[List], List]: + """Multi select validator returning list of selected values.""" + + def validator(selected: List) -> list: + """Return list of selected values.""" + if not isinstance(selected, list): + raise vol.Invalid("Not a list") + + for value in selected: + if value not in options: + raise vol.Invalid(f"{value} is not a valid option") + + return selected + + return validator + + def deprecated( key: str, replacement_key: Optional[str] = None, @@ -713,6 +730,9 @@ def custom_serializer(schema: Any) -> Any: if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} + if schema is multi_select: + return {"type": "multi_select"} + return voluptuous_serialize.UNSUPPORTED diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 57554d37bb1..afb215822c4 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -472,6 +472,22 @@ def test_datetime(): schema("2016-11-23T18:59:08") +def test_multi_select(): + """Test multi select validation. + + Expected behavior: + - Will not accept any input but a list + - Will not accept selections outside of configured scope + """ + schema = vol.Schema(cv.multi_select({"paulus": "Paulus", "robban": "Robban"})) + + with pytest.raises(vol.Invalid): + schema("robban") + schema(["paulus", "martinhj"]) + + schema(["robban", "paulus"]) + + @pytest.fixture def schema(): """Create a schema used for testing deprecation.""" From fbbb29a6ec91e91c5f051ec2cd7d50a0e6ffe74a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Feb 2020 13:43:07 -0800 Subject: [PATCH 237/378] Catch unexpected exceptions when validating config (#31795) --- homeassistant/config.py | 20 +++++++++ tests/test_config.py | 98 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index f5870d683a0..6ff571f0d6b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -697,6 +697,9 @@ async def async_process_component_config( except (vol.Invalid, HomeAssistantError) as ex: async_log_exception(ex, domain, config, hass, integration.documentation) return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error calling %s config validator", domain) + return None # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): @@ -705,6 +708,9 @@ async def async_process_component_config( except vol.Invalid as ex: async_log_exception(ex, domain, config, hass, integration.documentation) return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain) + return None component_platform_schema = getattr( component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None) @@ -721,6 +727,13 @@ async def async_process_component_config( except vol.Invalid as ex: async_log_exception(ex, domain, p_config, hass, integration.documentation) continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error validating %s platform config with %s component platform schema", + p_name, + domain, + ) + continue # Not all platform components follow same pattern for platforms # So if p_name is None we are not going to validate platform @@ -756,6 +769,13 @@ async def async_process_component_config( p_integration.documentation, ) continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error validating config for %s platform for %s component with PLATFORM_SCHEMA", + p_name, + domain, + ) + continue platforms.append(p_validated) diff --git a/tests/test_config.py b/tests/test_config.py index 319ef841f46..fc5ec43093b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,9 +4,11 @@ import asyncio from collections import OrderedDict import copy import os -import unittest.mock as mock +from unittest import mock +from unittest.mock import Mock import asynctest +from asynctest import CoroutineMock, patch import pytest from voluptuous import Invalid, MultipleInvalid import yaml @@ -893,3 +895,97 @@ async def test_merge_split_component_definition(hass): assert len(config["light one"]) == 1 assert len(config["light two"]) == 1 assert len(config["light three"]) == 1 + + +async def test_component_config_exceptions(hass, caplog): + """Test unexpected exceptions validating component config.""" + # Config validator + assert ( + await config_util.async_process_component_config( + hass, + {}, + integration=Mock( + domain="test_domain", + get_platform=Mock( + return_value=Mock( + async_validate_config=CoroutineMock( + side_effect=ValueError("broken") + ) + ) + ), + ), + ) + is None + ) + assert "ValueError: broken" in caplog.text + assert "Unknown error calling test_domain config validator" in caplog.text + + # component.CONFIG_SCHEMA + caplog.clear() + assert ( + await config_util.async_process_component_config( + hass, + {}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock( + CONFIG_SCHEMA=Mock(side_effect=ValueError("broken")) + ) + ), + ), + ) + is None + ) + assert "ValueError: broken" in caplog.text + assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text + + # component.PLATFORM_SCHEMA + caplog.clear() + assert await config_util.async_process_component_config( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock( + spec=["PLATFORM_SCHEMA_BASE"], + PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), + ) + ), + ), + ) == {"test_domain": []} + assert "ValueError: broken" in caplog.text + assert ( + "Unknown error validating test_platform platform config with test_domain component platform schema" + in caplog.text + ) + + # platform.PLATFORM_SCHEMA + caplog.clear() + with patch( + "homeassistant.config.async_get_integration_with_requirements", + return_value=Mock( # integration that owns platform + get_platform=Mock( + return_value=Mock( # platform + PLATFORM_SCHEMA=Mock(side_effect=ValueError("broken")) + ) + ) + ), + ): + assert await config_util.async_process_component_config( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + ), + ) == {"test_domain": []} + assert "ValueError: broken" in caplog.text + assert ( + "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA" + in caplog.text + ) From 3018e8ff47c25c5aeb04ed3f7b197a804906fdcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 13 Feb 2020 23:57:07 +0200 Subject: [PATCH 238/378] Use time.monotonic instead of time.time where appropriate (#31780) --- homeassistant/bootstrap.py | 6 +++--- homeassistant/components/bme680/sensor.py | 8 ++++---- homeassistant/components/doods/image_processing.py | 4 ++-- homeassistant/components/maxcube/__init__.py | 6 +++--- homeassistant/components/netatmo/sensor.py | 2 -- homeassistant/components/proxmoxve/__init__.py | 4 ++-- homeassistant/components/verisure/lock.py | 6 +++--- homeassistant/components/verisure/switch.py | 8 ++++---- 8 files changed, 21 insertions(+), 23 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0b7dbe370be..4d69698f252 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -4,7 +4,7 @@ import logging import logging.handlers import os import sys -from time import time +from time import monotonic from typing import Any, Dict, Optional, Set import voluptuous as vol @@ -110,7 +110,7 @@ async def async_from_config_dict( Dynamically loads required components and its dependencies. This method is a coroutine. """ - start = time() + start = monotonic() core_config = config.get(core.DOMAIN, {}) @@ -131,7 +131,7 @@ async def async_from_config_dict( await _async_set_up_integrations(hass, config) - stop = time() + stop = monotonic() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 65c87890242..43430f724bb 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -1,7 +1,7 @@ """Support for BME680 Sensor over SMBus.""" import logging import threading -from time import sleep, time +from time import monotonic, sleep import bme680 # pylint: disable=import-error from smbus import SMBus # pylint: disable=import-error @@ -240,15 +240,15 @@ class BME680Handler: # Pause to allow initial data read for device validation. sleep(1) - start_time = time() - curr_time = time() + start_time = monotonic() + curr_time = monotonic() burn_in_data = [] _LOGGER.info( "Beginning %d second gas sensor burn in for Air Quality", burn_in_time ) while curr_time - start_time < burn_in_time: - curr_time = time() + curr_time = monotonic() if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: gas_resistance = self._sensor.data.gas_resistance burn_in_data.append(gas_resistance) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 9525f9e8ddf..65a32938140 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -285,7 +285,7 @@ class Doods(ImageProcessingEntity): ) # Run detection - start = time.time() + start = time.monotonic() response = self._doods.detect( image, dconfig=self._dconfig, detector_name=self._detector_name ) @@ -293,7 +293,7 @@ class Doods(ImageProcessingEntity): "doods detect: %s response: %s duration: %s", self._dconfig, response, - time.time() - start, + time.monotonic() - start, ) matches = {} diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 1b65cb161e1..3e6ecbc948b 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -90,14 +90,14 @@ class MaxCubeHandle: self.cube = cube self.scan_interval = scan_interval self.mutex = Lock() - self._updatets = time.time() + self._updatets = time.monotonic() def update(self): """Pull the latest data from the MAX! Cube.""" # Acquire mutex to prevent simultaneous update from multiple threads with self.mutex: # Only update every update_interval - if (time.time() - self._updatets) >= self.scan_interval: + if (time.monotonic() - self._updatets) >= self.scan_interval: _LOGGER.debug("Updating") try: @@ -106,6 +106,6 @@ class MaxCubeHandle: _LOGGER.error("Max!Cube connection failed") return False - self._updatets = time.time() + self._updatets = time.monotonic() else: _LOGGER.debug("Skipping update") diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index afdb7c053f3..818662ee69c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,7 +1,6 @@ """Support for the Netatmo Weather Service.""" from datetime import timedelta import logging -from time import time import pyatmo @@ -519,7 +518,6 @@ class NetatmoData: """Initialize the data object.""" self.data = {} self.station_data = station_data - self._next_update = time() self.auth = auth def get_module_infos(self): diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 58cb50ee304..315fb8b1c91 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -147,12 +147,12 @@ class ProxmoxClient: verify_ssl=self._verify_ssl, ) - self._connection_start_time = time.time() + self._connection_start_time = time.monotonic() def get_api_client(self): """Return the ProxmoxAPI client and rebuild it if necessary.""" - connection_age = time.time() - self._connection_start_time + connection_age = time.monotonic() - self._connection_start_time # Workaround for the Proxmoxer bug where the connection stops working after some time if connection_age > 30 * 60: diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 01eb5faf897..5b5d50347ac 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -1,6 +1,6 @@ """Support for Verisure locks.""" import logging -from time import sleep, time +from time import monotonic, sleep from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED @@ -71,7 +71,7 @@ class VerisureDoorlock(LockDevice): def update(self): """Update lock status.""" - if time() - self._change_timestamp < 10: + if monotonic() - self._change_timestamp < 10: return hub.update_overview() status = hub.get_first( @@ -131,4 +131,4 @@ class VerisureDoorlock(LockDevice): transaction = hub.session.get_lock_state_transaction(transaction_id) if transaction["result"] == "OK": self._state = state - self._change_timestamp = time() + self._change_timestamp = monotonic() diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 32e1c1364a3..2df250303c5 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,6 +1,6 @@ """Support for Verisure Smartplugs.""" import logging -from time import time +from time import monotonic from homeassistant.components.switch import SwitchDevice @@ -44,7 +44,7 @@ class VerisureSmartplug(SwitchDevice): @property def is_on(self): """Return true if on.""" - if time() - self._change_timestamp < 10: + if monotonic() - self._change_timestamp < 10: return self._state self._state = ( hub.get_first( @@ -67,13 +67,13 @@ class VerisureSmartplug(SwitchDevice): """Set smartplug status on.""" hub.session.set_smartplug_state(self._device_label, True) self._state = True - self._change_timestamp = time() + self._change_timestamp = monotonic() def turn_off(self, **kwargs): """Set smartplug status off.""" hub.session.set_smartplug_state(self._device_label, False) self._state = False - self._change_timestamp = time() + self._change_timestamp = monotonic() # pylint: disable=no-self-use def update(self): From f6341ed3de9ba4616b4f84acdbc8f67270257b54 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 13 Feb 2020 15:44:45 -0800 Subject: [PATCH 239/378] =?UTF-8?q?Add=20Home=20Assistant=20Companion=20to?= =?UTF-8?q?=20manifest.json=20so=20we=20can=20sugges=E2=80=A6=20(#31808)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/frontend/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b26d7a4e168..6521901d692 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -62,6 +62,10 @@ MANIFEST_JSON = { "short_name": "Assistant", "start_url": "/?homescreen=1", "theme_color": DEFAULT_THEME_COLOR, + "prefer_related_applications": True, + "related_applications": [ + {"platform": "play", "id": "io.homeassistant.companion.android"} + ], } DATA_PANELS = "frontend_panels" From 59932545ab415f5ba66c4740ba8182cc415f2ab2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 13 Feb 2020 17:45:48 -0600 Subject: [PATCH 240/378] Update Plex connection class to push (#31806) --- homeassistant/components/plex/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index d38d13c847e..0cbdd4679a9 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -52,7 +52,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback From ebeab7f44c45bcc77ee2db35123c3c2bf344c134 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Feb 2020 16:15:46 -0800 Subject: [PATCH 241/378] Update codecov.yml --- codecov.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index be739b61809..8c4d0b29ff3 100644 --- a/codecov.yml +++ b/codecov.yml @@ -13,4 +13,6 @@ coverage: url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg=" comment: require_changes: yes - branches: master + branches: + - master + - !dev From 2f3ab15268f2ba275c6c06d1ed2d088113bd0dff Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 14 Feb 2020 01:24:43 +0100 Subject: [PATCH 242/378] Remove force from async_schedule_update_ha_state for HMIPC (#31796) * remove force from async_schedule_update_ha_state if HMIPC * Fix lint --- .../homematicip_cloud/alarm_control_panel.py | 13 +++++++++++-- .../homematicip_cloud/test_alarm_control_panel.py | 3 +++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 902755bfa07..f5316350091 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN @@ -95,10 +96,18 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): """Register callbacks.""" self._home.on_update(self._async_device_changed) + @callback def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" - _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) - self.async_schedule_update_ha_state(True) + # Don't update disabled entities + if self.enabled: + _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) + self.async_schedule_update_ha_state() + else: + _LOGGER.debug( + "Device Changed Event for %s (Alarm Control Panel) not fired. Entity is disabled.", + self.name, + ) @property def name(self) -> str: diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index cf85e805143..ae07b951c63 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -31,6 +31,9 @@ async def _async_manipulate_security_zones( internal_zone = home.search_group_by_id(internal_zone_id) internal_zone.active = internal_active + home.from_json(json) + home._get_functionalHomes(json) + home._load_functionalChannels() home.fire_update_event(json) await hass.async_block_till_done() From 32bc94bdd6828f206d673e9a59852015aa98cf9d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 14 Feb 2020 00:31:49 +0000 Subject: [PATCH 243/378] [ci skip] Translation update --- .../components/axis/.translations/en.json | 2 +- .../homekit_controller/.translations/en.json | 2 +- .../konnected/.translations/de.json | 66 ++++++++++++ .../konnected/.translations/es.json | 100 ++++++++++++++++++ .../konnected/.translations/lb.json | 75 +++++++++++++ .../konnected/.translations/no.json | 100 ++++++++++++++++++ .../konnected/.translations/ru.json | 38 ++++++- .../konnected/.translations/sv.json | 44 +++++++- .../konnected/.translations/zh-Hant.json | 100 ++++++++++++++++++ .../components/melcloud/.translations/de.json | 19 ++++ .../components/melcloud/.translations/es.json | 23 ++++ .../components/melcloud/.translations/lb.json | 18 ++++ .../components/melcloud/.translations/no.json | 23 ++++ .../melcloud/.translations/zh-Hant.json | 23 ++++ .../minecraft_server/.translations/es.json | 24 +++++ .../minecraft_server/.translations/lb.json | 17 +++ .../components/plex/.translations/en.json | 2 +- .../components/vilfo/.translations/ca.json | 16 +++ .../components/vilfo/.translations/de.json | 13 +++ .../components/vilfo/.translations/es.json | 23 ++++ .../components/vilfo/.translations/lb.json | 23 ++++ .../components/vilfo/.translations/no.json | 23 ++++ .../components/vilfo/.translations/ru.json | 23 ++++ .../vilfo/.translations/zh-Hant.json | 23 ++++ .../components/vizio/.translations/en.json | 2 +- 25 files changed, 814 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/konnected/.translations/de.json create mode 100644 homeassistant/components/konnected/.translations/es.json create mode 100644 homeassistant/components/konnected/.translations/lb.json create mode 100644 homeassistant/components/konnected/.translations/no.json create mode 100644 homeassistant/components/konnected/.translations/zh-Hant.json create mode 100644 homeassistant/components/melcloud/.translations/de.json create mode 100644 homeassistant/components/melcloud/.translations/es.json create mode 100644 homeassistant/components/melcloud/.translations/lb.json create mode 100644 homeassistant/components/melcloud/.translations/no.json create mode 100644 homeassistant/components/melcloud/.translations/zh-Hant.json create mode 100644 homeassistant/components/minecraft_server/.translations/es.json create mode 100644 homeassistant/components/minecraft_server/.translations/lb.json create mode 100644 homeassistant/components/vilfo/.translations/ca.json create mode 100644 homeassistant/components/vilfo/.translations/de.json create mode 100644 homeassistant/components/vilfo/.translations/es.json create mode 100644 homeassistant/components/vilfo/.translations/lb.json create mode 100644 homeassistant/components/vilfo/.translations/no.json create mode 100644 homeassistant/components/vilfo/.translations/ru.json create mode 100644 homeassistant/components/vilfo/.translations/zh-Hant.json diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index abc1e2f17ec..1f00800422c 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", - "bad_config_file": "Bad data from config file", + "bad_config_file": "Bad data from configuration file", "link_local_address": "Link local addresses are not supported", "not_axis_device": "Discovered device not an Axis device", "updated_configuration": "Updated device configuration with new host address" diff --git a/homeassistant/components/homekit_controller/.translations/en.json b/homeassistant/components/homekit_controller/.translations/en.json index 31731a52203..72aa720b449 100644 --- a/homeassistant/components/homekit_controller/.translations/en.json +++ b/homeassistant/components/homekit_controller/.translations/en.json @@ -6,7 +6,7 @@ "already_in_progress": "Config flow for device is already in progress.", "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", - "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.", + "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "no_devices": "No unpaired devices could be found" }, "error": { diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json new file mode 100644 index 00000000000..02b488b7deb --- /dev/null +++ b/homeassistant/components/konnected/.translations/de.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t", + "unknown": "Unbekannter Fehler ist aufgetreten" + }, + "step": { + "confirm": { + "title": "Konnected Device Bereit" + }, + "user": { + "data": { + "host": "Konnected Ger\u00e4t IP-Adresse", + "port": "Konnected Device Port" + } + } + }, + "title": "Konnected.io" + }, + "options": { + "step": { + "options_binary": { + "data": { + "name": "Name (optional)" + } + }, + "options_digital": { + "data": { + "name": "Name (optional)", + "type": "Sensortyp" + } + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + } + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + } + }, + "options_switch": { + "data": { + "name": "Name (optional)" + } + } + }, + "title": "Konnected Alarm Panel-Optionen" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json new file mode 100644 index 00000000000..c80d3854dd8 --- /dev/null +++ b/homeassistant/components/konnected/.translations/es.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en curso.", + "not_konn_panel": "No es un dispositivo Konnected.io reconocido", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "cannot_connect": "No se puede conectar a un Panel conectado en {host}:{port}" + }, + "step": { + "confirm": { + "description": "Modelo: {model}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del panel de alarmas Konnected.", + "title": "Dispositivo Konnected Listo" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP del dispositivo Konnected", + "port": "Puerto del dispositivo Konnected" + }, + "description": "Introduzca la informaci\u00f3n del host de su panel Konnected.", + "title": "Descubrir el dispositivo Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "No es un dispositivo Konnected.io reconocido" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertir el estado de apertura/cierre", + "name": "Nombre (opcional)", + "type": "Tipo de sensor binario" + }, + "description": "Seleccione las opciones para el sensor binario conectado a {zone}", + "title": "Configurar sensor binario" + }, + "options_digital": { + "data": { + "name": "Nombre (opcional)", + "poll_interval": "Intervalo de sondeo (minutos) (opcional)", + "type": "Tipo de sensor" + }, + "description": "Seleccione las opciones para el sensor digital conectado a {zone}", + "title": "Configurar el sensor digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "OUT" + }, + "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada E / S a continuaci\u00f3n: seg\u00fan la E / S, puede permitir sensores binarios (contactos de apertura / cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARMA1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S extendidas" + }, + "options_misc": { + "data": { + "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado" + }, + "description": "Seleccione el comportamiento deseado para su panel", + "title": "Configurar miscel\u00e1neos" + }, + "options_switch": { + "data": { + "activation": "Salida cuando est\u00e1 activada", + "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "name": "Nombre (opcional)", + "pause": "Pausa entre pulsos (ms) (opcional)", + "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" + }, + "description": "Por favor, seleccione las opciones de salida para {zone}", + "title": "Configurar la salida conmutable" + } + }, + "title": "Opciones del panel de alarma Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/lb.json b/homeassistant/components/konnected/.translations/lb.json new file mode 100644 index 00000000000..4f368f940a8 --- /dev/null +++ b/homeassistant/components/konnected/.translations/lb.json @@ -0,0 +1,75 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf fir den Apparat ass schonn am gaangen.", + "not_konn_panel": "Keen erkannten Konnected.io Apparat", + "unknown": "Onbekannten Fehler opgetrueden" + }, + "error": { + "cannot_connect": "Kann sech net mam Konnected Panel um {host}:{port} verbannen" + }, + "step": { + "user": { + "title": "Konnected Apparat entdecken" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Keen erkannten Konnected.io Apparat" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Op/Zou Zoustand vertauschen", + "name": "Numm (optional)", + "type": "Typ vun Bin\u00e4re Sensor" + }, + "title": "Bin\u00e4re Sensor konfigur\u00e9ieren" + }, + "options_digital": { + "data": { + "name": "Numm (optional)", + "type": "Typ vum Sensor" + }, + "title": "Digitale Sensor konfigur\u00e9ieren" + }, + "options_io": { + "data": { + "1": "Zon 2", + "2": "Zon 1", + "3": "Zon 3", + "4": "Zon 4", + "5": "Zon 5", + "6": "Zon 6", + "7": "Zon 7", + "out": "OUT" + }, + "title": "I/O konfigur\u00e9ieren" + }, + "options_io_ext": { + "data": { + "10": "Zon 10", + "11": "Zon 11", + "12": "Zon 12", + "8": "Zon 8", + "9": "Zon 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + } + }, + "options_switch": { + "data": { + "activation": "Ausgang wann un", + "momentary": "Pulsatiounsdauer (ms) (optional)", + "name": "Numm (optional)" + }, + "title": "\u00cbmschltbaren Ausgang konfigur\u00e9ieren" + } + }, + "title": "Konnected Alarm Panneau Optiounen" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json new file mode 100644 index 00000000000..569dac5756f --- /dev/null +++ b/homeassistant/components/konnected/.translations/no.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.", + "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet", + "unknown": "Ukjent feil oppstod" + }, + "error": { + "cannot_connect": "Kan ikke koble til et Konnected Panel p\u00e5 {host} : {port}" + }, + "step": { + "confirm": { + "description": "Modell: {model}\nVert: {host}\nPort: {port}\n\nDu kan konfigurere IO og panel atferd i Konnected Alarm Panel innstillinger.", + "title": "Konnected Enhet klar" + }, + "user": { + "data": { + "host": "Konnected enhet IP-adresse", + "port": "Koblet enhetsport" + }, + "description": "Vennligst skriv inn verten informasjon for din Konnected Panel.", + "title": "Oppdag Konnected Enheten" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverter \u00e5pen / lukk tilstand", + "name": "Navn (valgfritt)", + "type": "Bin\u00e6r sensortype" + }, + "description": "Vennligst velg alternativer for bin\u00e6re sensor koblet til {sone}", + "title": "Konfigurer bin\u00e6r sensor" + }, + "options_digital": { + "data": { + "name": "Navn (valgfritt)", + "poll_interval": "Avstemningsintervall (minutter) (valgfritt)", + "type": "Sensortype" + }, + "description": "Vennligst velg alternativene for den digitale sensor som er koblet til {sone}", + "title": "Konfigurere Digital Sensor" + }, + "options_io": { + "data": { + "1": "Sone 1", + "2": "Sone 2", + "3": "Sone 3", + "4": "Sone 4", + "5": "Sone 5", + "6": "Sone 6", + "7": "Sone 7", + "out": "OUT" + }, + "description": "Oppdaget en {model} hos {host} . Velg basiskonfigurasjon for hver I / O nedenfor - avhengig av I / O kan det gi rom for bin\u00e6re sensorer (\u00e5pne / lukke kontakter), digitale sensorer (dht og ds18b20), eller switchbare utganger. Du vil kunne konfigurere detaljerte alternativer i de neste trinnene.", + "title": "Konfigurere I/O" + }, + "options_io_ext": { + "data": { + "10": "Sone 10", + "11": "Sone 11", + "12": "Sone 12", + "8": "Sone 8", + "9": "Sone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Velg konfigurasjonen av de gjenv\u00e6rende I/O nedenfor. Du vil v\u00e6re i stand til \u00e5 konfigurere detaljerte alternativer i de neste trinnene.", + "title": "Konfigurer utvidet I / O" + }, + "options_misc": { + "data": { + "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring" + }, + "description": "Vennligst velg \u00f8nsket atferd for din panel", + "title": "Konfigurere Diverse" + }, + "options_switch": { + "data": { + "activation": "Utgang n\u00e5r den er p\u00e5", + "momentary": "Pulsvarighet (ms) (valgfritt)", + "name": "Navn (valgfritt)", + "pause": "Pause mellom pulser (ms) (valgfritt)", + "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)" + }, + "description": "Velg outputalternativer for {zone}", + "title": "Konfigurere Valgbare Utgang" + } + }, + "title": "Alternativer for Konnected Alarm Panel" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json index e8cae967b50..59d4a199576 100644 --- a/homeassistant/components/konnected/.translations/ru.json +++ b/homeassistant/components/konnected/.translations/ru.json @@ -59,8 +59,42 @@ "7": "\u0417\u043e\u043d\u0430 7", "out": "\u0412\u042b\u0425\u041e\u0414" }, - "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {model} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {host}. \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432, \u043a \u043f\u0430\u043d\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 (dht \u0438 ds18b20) \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u044b\u0435 \u0432\u044b\u0445\u043e\u0434\u044b. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445." + "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {model} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {host}. \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432, \u043a \u043f\u0430\u043d\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 (dht \u0438 ds18b20) \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u044b\u0435 \u0432\u044b\u0445\u043e\u0434\u044b. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432" + }, + "options_io_ext": { + "data": { + "10": "\u0417\u043e\u043d\u0430 10", + "11": "\u0417\u043e\u043d\u0430 11", + "12": "\u0417\u043e\u043d\u0430 12", + "8": "\u0417\u043e\u043d\u0430 8", + "9": "\u0417\u043e\u043d\u0430 9", + "alarm1": "\u0422\u0420\u0415\u0412\u041e\u0413\u04101", + "alarm2_out2": "\u0412\u042b\u0425\u041e\u04142/\u0422\u0420\u0415\u0412\u041e\u0413\u04102", + "out1": "\u0412\u042b\u0425\u041e\u04141" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432" + }, + "options_misc": { + "data": { + "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0436\u0435\u043b\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438.", + "title": "\u041f\u0440\u043e\u0447\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "options_switch": { + "data": { + "activation": "\u0412\u044b\u0445\u043e\u0434 \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0445\u043e\u0434\u0430 \u0434\u043b\u044f {zone}.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u043e\u0433\u043e \u0432\u044b\u0445\u043e\u0434\u0430" } - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Konnected" } } \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/sv.json b/homeassistant/components/konnected/.translations/sv.json index f6702745542..abd15786aff 100644 --- a/homeassistant/components/konnected/.translations/sv.json +++ b/homeassistant/components/konnected/.translations/sv.json @@ -11,6 +11,7 @@ }, "step": { "confirm": { + "description": "Modell: {modell}\nV\u00e4rd: {host}\nPort: {port}\n\nDu kan konfigurera IO- och panelbeteendet i inst\u00e4llningarna f\u00f6r Konnected Alarm Panel.", "title": "Konnected-enheten redo" }, "user": { @@ -28,6 +29,10 @@ "abort": { "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet" }, + "error": { + "one": "Tom", + "other": "Tom" + }, "step": { "options_binary": { "data": { @@ -35,6 +40,7 @@ "name": "Namn (valfritt)", "type": "Bin\u00e4r sensortyp" }, + "description": "V\u00e4lj alternativ f\u00f6r den bin\u00e4ra sensorn som \u00e4r ansluten till {zone}", "title": "Konfigurera Bin\u00e4r Sensor" }, "options_digital": { @@ -42,20 +48,54 @@ "name": "Namn (valfritt)", "poll_interval": "H\u00e4mtningsintervall (minuter) (valfritt)", "type": "Sensortyp" - } + }, + "description": "V\u00e4lj alternativ f\u00f6r den digitala sensorn som \u00e4r ansluten till {zone}", + "title": "Konfigurera Digital Sensor" + }, + "options_io": { + "data": { + "1": "Zon 1", + "2": "Zon 2", + "3": "Zon 3", + "4": "Zon 4", + "5": "Zon 5", + "6": "Zon 6", + "7": "Zon 7", + "out": "UT" + }, + "description": "Uppt\u00e4ckte en {model} p\u00e5 {host}. V\u00e4lj baskonfigurationen f\u00f6r varje I/O nedan - beroende p\u00e5 I/O kan det m\u00f6jligg\u00f6ra bin\u00e4ra sensorer (\u00f6ppen/st\u00e4ngd kontakter), digitala sensorer (dht och ds18b20) eller omkopplingsbara utg\u00e5ngar. Du kan konfigurera detaljerade alternativ i n\u00e4sta steg.", + "title": "Konfigurera I/O" }, "options_io_ext": { + "data": { + "10": "Zon 10", + "11": "Zon 11", + "12": "Zon 12", + "8": "Zon 8", + "9": "Zon 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "V\u00e4lj den konfiguration av resterande I/O nedan. Du kommer att kunna konfigurera detaljerade alternativ i n\u00e4sta steg.", "title": "Konfigurera ut\u00f6kat I/O" }, "options_misc": { + "data": { + "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring" + }, "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", "title": "Konfigurera \u00d6vrigt" }, "options_switch": { "data": { + "activation": "Utdata n\u00e4r den \u00e4r p\u00e5", + "momentary": "Pulsvarighet (ms) (valfritt)", "name": "Namn (valfritt)", - "pause": "Paus mellan pulser (ms) (valfritt)" + "pause": "Paus mellan pulser (ms) (valfritt)", + "repeat": "G\u00e5nger att upprepa (-1=o\u00e4ndligt) (tillval)" }, + "description": "V\u00e4lj utdataalternativ f\u00f6r {zone}", "title": "Konfigurera v\u00e4xelbar utdata" } }, diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json new file mode 100644 index 00000000000..0ecd6c9fc25 --- /dev/null +++ b/homeassistant/components/konnected/.translations/zh-Hant.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Konnected \u9762\u677f\uff1a{host}:{port}" + }, + "step": { + "confirm": { + "description": "\u578b\u865f\uff1a{model}\n\u4e3b\u6a5f\u7aef\uff1a{host}\n\u901a\u8a0a\u57e0\uff1a{port}\n\n\u53ef\u4ee5\u65bc Konncected \u8b66\u5831\u9762\u677f\u8a2d\u5b9a\u4e2d\u8a2d\u5b9a IO \u8207\u9762\u677f\u884c\u70ba\u3002", + "title": "Konnected \u8a2d\u5099\u5df2\u5099\u59a5" + }, + "user": { + "data": { + "host": "Konnected \u8a2d\u5099 IP \u4f4d\u5740", + "port": "Konnected \u8a2d\u5099\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165 Konnected \u9762\u677f\u4e3b\u6a5f\u7aef\u8cc7\u8a0a\u3002", + "title": "\u641c\u7d22 Konnected \u8a2d\u5099" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099" + }, + "step": { + "options_binary": { + "data": { + "inverse": "\u53cd\u8f49\u958b\u555f/\u95dc\u9589\u72c0\u614b", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", + "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u578b" + }, + "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805", + "title": "\u8a2d\u5b9a\u4e8c\u9032\u4f4d\u611f\u61c9\u5668" + }, + "options_digital": { + "data": { + "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", + "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09", + "type": "\u611f\u61c9\u5668\u985e\u578b" + }, + "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u6578\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805", + "title": "\u8a2d\u5b9a\u6578\u4f4d\u611f\u61c9\u5668" + }, + "options_io": { + "data": { + "1": "\u5340\u57df 1", + "2": "\u5340\u57df 2", + "3": "\u5340\u57df 3", + "4": "\u5340\u57df 4", + "5": "\u5340\u57df 5", + "6": "\u5340\u57df 6", + "7": "\u5340\u57df 7", + "out": "OUT" + }, + "description": "\u65bc {host} \u767c\u73fe {model}\u3002\u8acb\u65bc\u4e0b\u65b9\u6bcf\u4e00\u500b I/O \u9078\u64c7\u57fa\u672c\u8a2d\u5b9a - \u96a8\u8457 I/O \u4e0d\u540c\uff0c\u53ef\u5141\u8a31\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\uff08\u958b\u555f/\u95dc\u9589\u72c0\u614b\uff09\u3001\u6578\u4f4d\u611f\u61c9\u5668\uff08DHT \u53ca ds18b20\uff09\uff0c\u6216\u8005\u53ef\u5207\u63db\u8f38\u51fa\u3002\u53ef\u4ee5\u65bc\u4e0b\u4e00\u6b65\u8a2d\u5b9a\u8a73\u7d30\u9078\u9805\u3002", + "title": "\u8a2d\u5b9a I/O" + }, + "options_io_ext": { + "data": { + "10": "\u5340\u57df 10", + "11": "\u5340\u57df 11", + "12": "\u5340\u57df 12", + "8": "\u5340\u57df 8", + "9": "\u5340\u57df 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "\u9078\u64c7\u4e0b\u65b9\u5269\u9918 I/O \u8a2d\u5b9a\u3002\u53ef\u4ee5\u65bc\u4e0b\u4e00\u6b65\u8a2d\u5b9a\u8a73\u7d30\u9078\u9805\u3002", + "title": "\u8a2d\u5b9a\u5ef6\u4f38 I/O" + }, + "options_misc": { + "data": { + "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED" + }, + "description": "\u8acb\u9078\u64c7\u9762\u677f\u671f\u671b\u884c\u70ba", + "title": "\u5176\u4ed6\u8a2d\u5b9a" + }, + "options_switch": { + "data": { + "activation": "\u958b\u555f\u6642\u8f38\u51fa", + "momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09\uff08\u9078\u9805\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", + "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", + "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" + }, + "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805", + "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa" + } + }, + "title": "Konnected \u8b66\u5831\u9762\u677f\u9078\u9805" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/de.json b/homeassistant/components/melcloud/.translations/de.json new file mode 100644 index 00000000000..15d2ef2ab9c --- /dev/null +++ b/homeassistant/components/melcloud/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "MELCloud Passwort." + }, + "description": "Verbinden Sie sich mit Ihrem MELCloud-Konto.", + "title": "Stellen Sie eine Verbindung zu MELCloud her" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/es.json b/homeassistant/components/melcloud/.translations/es.json new file mode 100644 index 00000000000..182f06c33c3 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3n mELCloud ya configurada para este correo electr\u00f3nico. Se ha actualizado el token de acceso." + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "invalid_auth": "Autentificaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a de MELCloud.", + "username": "Correo electr\u00f3nico utilizado para iniciar sesi\u00f3n en MELCloud." + }, + "description": "Con\u00e9ctate usando tu cuenta de MELCloud.", + "title": "Con\u00e9ctese a MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/lb.json b/homeassistant/components/melcloud/.translations/lb.json new file mode 100644 index 00000000000..7d94fbb53f8 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "MELCloud Passwuert" + }, + "title": "Mat MELCloud verbannen" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/no.json b/homeassistant/components/melcloud/.translations/no.json new file mode 100644 index 00000000000..a464bbfda19 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud integrasjon er allerede konfigurert p\u00e5 denne e-posten. Access token har blitt oppdatert." + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "MELCloud passord.", + "username": "E-post som blir brukt til \u00e5 logge inn p\u00e5 MELCloud." + }, + "description": "Koble til ved hjelp av MELCloud-kontoen din.", + "title": "Koble til MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/zh-Hant.json b/homeassistant/components/melcloud/.translations/zh-Hant.json new file mode 100644 index 00000000000..c098d041598 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u5bc6\u9470\u5df2\u66f4\u65b0\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "MELCloud \u5bc6\u78bc\u3002", + "username": "MELCloud \u767b\u5165\u90f5\u4ef6\u3002" + }, + "description": "\u4f7f\u7528 MELCloud \u5e33\u865f\u9032\u884c\u9023\u7dda\u3002", + "title": "\u9023\u7dda\u81f3 MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/es.json b/homeassistant/components/minecraft_server/.translations/es.json new file mode 100644 index 00000000000..14831ef45e1 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar al servidor. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Tambi\u00e9n aseg\u00farese de que est\u00e1 ejecutando al menos Minecraft versi\u00f3n 1.7 en su servidor.", + "invalid_ip": "La direcci\u00f3n IP no es valida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "port": "Puerto" + }, + "description": "Configura tu instancia de Minecraft Server para permitir la supervisi\u00f3n.", + "title": "Enlace su servidor Minecraft" + } + }, + "title": "Servidor Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/lb.json b/homeassistant/components/minecraft_server/.translations/lb.json new file mode 100644 index 00000000000..ddb3daf8523 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "port": "Port" + } + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 31211182f47..b75589e3a81 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,7 +4,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "discovery_no_file": "No legacy config file found", + "discovery_no_file": "No legacy configuration file found", "invalid_import": "Imported configuration is invalid", "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", diff --git a/homeassistant/components/vilfo/.translations/ca.json b/homeassistant/components/vilfo/.translations/ca.json new file mode 100644 index 00000000000..e788da3b552 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'encaminador Vilfo ja est\u00e0 configurat." + }, + "step": { + "user": { + "data": { + "host": "Nom d'amfitri\u00f3 o IP de l'encaminador" + }, + "title": "Connexi\u00f3 amb l'encaminador Vilfo" + } + }, + "title": "Encaminador Vilfo" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/de.json b/homeassistant/components/vilfo/.translations/de.json new file mode 100644 index 00000000000..d71300efe43 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Router-Hostname oder IP" + }, + "title": "Stellen Sie eine Verbindung zum Vilfo Router her" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/es.json b/homeassistant/components/vilfo/.translations/es.json new file mode 100644 index 00000000000..abf331a955a --- /dev/null +++ b/homeassistant/components/vilfo/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Este router Vilfo ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar. Compruebe la informaci\u00f3n que proporcion\u00f3 e int\u00e9ntelo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida. Compruebe el token de acceso e int\u00e9ntelo de nuevo.", + "unknown": "Se ha producido un error inesperado al configurar la integraci\u00f3n." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso para la API de Vilfo Router", + "host": "Nombre de host o IP del router" + }, + "description": "Configure la integraci\u00f3n de Vilfo Router. Necesita su nombre de host/IP de Vilfo Router y un token de acceso a la API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo", + "title": "Conectar con el Router Vilfo" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/lb.json b/homeassistant/components/vilfo/.translations/lb.json new file mode 100644 index 00000000000..7b88bd31d17 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Vilfo Router ass scho konfigur\u00e9iert." + }, + "error": { + "cannot_connect": "Feeler beim verbannen. Iwwerpr\u00e9ift \u00e4r Informatiounen an prob\u00e9iert nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun. Iwwerpr\u00e9ift den Acc\u00e8s jeton an prob\u00e9iert nach emol.", + "unknown": "Onerwaarte Feeler beim ariichten vun der Integratioun." + }, + "step": { + "user": { + "data": { + "access_token": "Acc\u00e8s Jeton fir Vilfo Router API", + "host": "Router Numm oder IP" + }, + "description": "Vilfo Router Integratioun ariichten. Dir braucht \u00e4re Vilfo Router Numm/IP an een API Acc\u00e8s Jeton. Fir weider Informatiounen zu d\u00ebser Integratioun a w\u00e9i een zu d\u00ebsen n\u00e9idegen Informatioune k\u00ebnnt, gitt op: https://www.home-assistant.io/integrations/vilfo", + "title": "Mam Vilfo Router verbannen" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/no.json b/homeassistant/components/vilfo/.translations/no.json new file mode 100644 index 00000000000..af72a4bd7b0 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Vilfo Ruteren er allerede konfigurert." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes. Vennligst sjekk informasjonen du oppga, og pr\u00f8v igjen.", + "invalid_auth": "Ugyldig godkjenning. Vennligst sjekk access token, og pr\u00f8v p\u00e5 nytt.", + "unknown": "Det oppstod en uventet feil under installasjonen av integrasjonen." + }, + "step": { + "user": { + "data": { + "access_token": "Tilgangstoken for Vilfo Router API", + "host": "Ruter vertsnavn eller IP" + }, + "description": "Konfigurer Vilfo Router-integreringen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. Hvis du vil ha mer informasjon om denne integreringen og hvordan du f\u00e5r disse detaljene, kan du g\u00e5 til: https://www.home-assistant.io/integrations/vilfo", + "title": "Koble til Vilfo Ruteren" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/ru.json b/homeassistant/components/vilfo/.translations/ru.json new file mode 100644 index 00000000000..ce8f325e0ea --- /dev/null +++ b/homeassistant/components/vilfo/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a API \u0440\u043e\u0443\u0442\u0435\u0440\u0430", + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 API. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", + "title": "Vilfo Router" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/zh-Hant.json b/homeassistant/components/vilfo/.translations/zh-Hant.json new file mode 100644 index 00000000000..7553cc683cd --- /dev/null +++ b/homeassistant/components/vilfo/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Vilfo \u8def\u7531\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u8f38\u5165\u8cc7\u6599\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_auth": "\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u5b58\u53d6\u5bc6\u9470\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "unknown": "\u8a2d\u5b9a\u6574\u5408\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u9470", + "host": "\u8def\u7531\u5668\u4e3b\u6a5f\u7aef\u6216 IP \u4f4d\u5740" + }, + "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u5bc6\u9470\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo", + "title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668" + } + }, + "title": "Vilfo \u8def\u7531\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index 80d8f500615..23b7c03d423 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", "host_exists": "Vizio component with host already configured.", "name_exists": "Vizio component with name already configured.", - "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly.", + "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly.", "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.", "updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly." }, From eb3097506f3e4c3e6edb991bbe266750aa1d8ace Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 13 Feb 2020 18:34:12 -0600 Subject: [PATCH 244/378] Add summary attribtue for currently playing media (#31803) --- homeassistant/components/plex/media_player.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1e8d0e6cfd2..003ad23f977 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -125,6 +125,7 @@ class PlexMediaPlayer(MediaPlayerDevice): 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 @@ -166,6 +167,7 @@ class PlexMediaPlayer(MediaPlayerDevice): 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 @@ -250,6 +252,7 @@ class PlexMediaPlayer(MediaPlayerDevice): self._session_type = self.session.type self._media_duration = int(self.session.duration / 1000) # title (movie name, tv episode name, music song name) + self._media_summary = self.session.summary self._media_title = self.session.title # media type self._set_media_type() @@ -439,6 +442,11 @@ class PlexMediaPlayer(MediaPlayerDevice): """Return the image URL of current playing media.""" return self._media_image_url + @property + def media_summary(self): + """Return the summary of current playing media.""" + return self._media_summary + @property def media_title(self): """Return the title of current playing media.""" @@ -712,6 +720,7 @@ class PlexMediaPlayer(MediaPlayerDevice): "media_content_rating": self._media_content_rating, "session_username": self.username, "media_library_name": self._app_name, + "summary": self.media_summary, } return attr From f0b2d50e41272de95f3f047678bd3391cafeb428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Fri, 14 Feb 2020 01:34:42 +0100 Subject: [PATCH 245/378] Fix swap of min and max default values in Modbus climate (#31801) --- homeassistant/components/modbus/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 29b6eb1a9fb..18d928e9057 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -56,8 +56,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PRECISION, default=1): cv.positive_int, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP, default=5): cv.positive_int, - vol.Optional(CONF_MIN_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), vol.Optional(CONF_UNIT, default="C"): cv.string, } From 408b41ea020abfd70e8e11e0663c96ab35e183b5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 13 Feb 2020 18:37:52 -0600 Subject: [PATCH 246/378] Add device registry support for Plex (#31797) * Add device registry support for Plex * Fix model * Use Plex server as sensor device identifier --- homeassistant/components/plex/media_player.py | 20 +++++++++++++++++++ homeassistant/components/plex/sensor.py | 14 +++++++++++++ homeassistant/components/plex/server.py | 8 ++++++++ tests/components/plex/mock_classes.py | 5 +++++ 4 files changed, 47 insertions(+) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 003ad23f977..cd94bb49632 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -109,8 +109,10 @@ class PlexMediaPlayer(MediaPlayerDevice): 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 @@ -195,8 +197,10 @@ class PlexMediaPlayer(MediaPlayerDevice): 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 @@ -216,6 +220,7 @@ class PlexMediaPlayer(MediaPlayerDevice): self._player_state = session_device.state 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") @@ -724,3 +729,18 @@ class PlexMediaPlayer(MediaPlayerDevice): } return attr + + @property + def device_info(self): + """Return a device description for device registry.""" + if self.machine_identifier is None: + return None + + return { + "identifiers": {(PLEX_DOMAIN, self.machine_identifier)}, + "manufacturer": "Plex", + "model": self._device_product or self._device_platform or self.make, + "name": self.name, + "sw_version": self._device_version, + "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier), + } diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 4fe6ed444ef..1caf8ec5f75 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -142,3 +142,17 @@ class PlexSensor(Entity): now_playing.append((now_playing_user, now_playing_title)) self._state = len(self.sessions) self._now_playing = now_playing + + @property + def device_info(self): + """Return a device description for device registry.""" + if self.unique_id is None: + return None + + return { + "identifiers": {(PLEX_DOMAIN, self._server.machine_identifier)}, + "manufacturer": "Plex", + "model": "Plex Media Server", + "name": "Activity Sensor", + "sw_version": self._server.version, + } diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index fcc7e5dda17..fe453ef2e9e 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -52,6 +52,7 @@ class PlexServer: self.options = options self.server_choice = None self._owner_username = None + self._version = None # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -102,6 +103,8 @@ class PlexServer: if owner_account: self._owner_username = owner_account[0] + self._version = self._plex_server.version + def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" @@ -196,6 +199,11 @@ class PlexServer: """Return the Plex server owner username.""" return self._owner_username + @property + def version(self): + """Return the version of the Plex server.""" + return self._version + @property def friendly_name(self): """Return name of connected Plex server.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index ed354138cb2..c4fccb35bb0 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -87,3 +87,8 @@ class MockPlexServer: def url_in_use(self): """Return URL used by PlexServer.""" return self._baseurl + + @property + def version(self): + """Mock version of PlexServer.""" + return "1.0" From f9fda7d616c41f2ecc76799e10b88fa8b3fdbc4f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 13 Feb 2020 19:47:16 -0600 Subject: [PATCH 247/378] update directv to directpy==0.6 (#31812) --- 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 adf05621a2c..b0f0f8bb5eb 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": ["directpy==0.5"], + "requirements": ["directpy==0.6"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index fd11709ec5e..39b1194cce0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -429,7 +429,7 @@ deluge-client==1.7.1 denonavr==0.7.12 # homeassistant.components.directv -directpy==0.5 +directpy==0.6 # homeassistant.components.discogs discogs_client==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37fa61b323c..0056721f04e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ defusedxml==0.6.0 denonavr==0.7.12 # homeassistant.components.directv -directpy==0.5 +directpy==0.6 # homeassistant.components.updater distro==1.4.0 From 9eb04152342609b076837ad8d8fef380f1c46251 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 14 Feb 2020 07:56:17 +0100 Subject: [PATCH 248/378] Speed up tests of HomematicIP Cloud (#31810) * Speed up tests of HomematicIP Cloud * load the json once --- .../components/homematicip_cloud/conftest.py | 52 ++-------- tests/components/homematicip_cloud/helper.py | 84 ++++++++++++++-- .../test_alarm_control_panel.py | 9 +- .../homematicip_cloud/test_binary_sensor.py | 98 +++++++++++++------ .../homematicip_cloud/test_climate.py | 40 +++++--- .../homematicip_cloud/test_cover.py | 21 ++-- .../homematicip_cloud/test_device.py | 84 ++++++++++------ .../components/homematicip_cloud/test_hap.py | 5 +- .../homematicip_cloud/test_light.py | 28 ++++-- .../homematicip_cloud/test_sensor.py | 77 ++++++++++----- .../homematicip_cloud/test_switch.py | 26 +++-- .../homematicip_cloud/test_weather.py | 25 ++--- 12 files changed, 366 insertions(+), 183 deletions(-) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b0b06447f8a..be67e53c02c 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,5 +1,5 @@ """Initializer helpers for HomematicIP fake server.""" -from asynctest import CoroutineMock, MagicMock, Mock, patch +from asynctest import CoroutineMock, MagicMock, Mock from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -19,9 +19,8 @@ from homeassistant.components.homematicip_cloud.const import ( from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.setup import async_setup_component -from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate +from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory from tests.common import MockConfigEntry @@ -66,46 +65,12 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: return config_entry -@pytest.fixture(name="default_mock_home") -def default_mock_home_fixture(mock_connection) -> AsyncHome: - """Create a fake homematic async home.""" - return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() - - -@pytest.fixture(name="default_mock_hap") -async def default_mock_hap_fixture( +@pytest.fixture(name="default_mock_hap_factory") +async def default_mock_hap_factory_fixture( hass: HomeAssistantType, mock_connection, hmip_config_entry ) -> HomematicipHAP: """Create a mocked homematic access point.""" - return await get_mock_hap(hass, mock_connection, hmip_config_entry) - - -async def get_mock_hap( - hass: HomeAssistantType, - mock_connection, - hmip_config_entry: config_entries.ConfigEntry, -) -> HomematicipHAP: - """Create a mocked homematic access point.""" - home_name = hmip_config_entry.data["name"] - mock_home = ( - HomeTemplate(connection=mock_connection, home_name=home_name) - .init_home() - .get_async_home_mock() - ) - - hmip_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.get_hap", - return_value=mock_home, - ): - assert await async_setup_component(hass, HMIPC_DOMAIN, {}) is True - - await hass.async_block_till_done() - - hap = hass.data[HMIPC_DOMAIN][HAPID] - mock_home.on_update(hap.async_update) - mock_home.on_create(hap.async_create_entity) - return hap + return HomeFactory(hass, mock_connection, hmip_config_entry) @pytest.fixture(name="hmip_config") @@ -130,13 +95,14 @@ def dummy_config_fixture() -> ConfigType: @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( - hass: HomeAssistantType, default_mock_hap, dummy_config + hass: HomeAssistantType, default_mock_hap_factory, dummy_config ) -> HomematicipHAP: """Create a fake homematic access point with hass services.""" + mock_hap = await default_mock_hap_factory.async_get_mock_hap() await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() - hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap} - return default_mock_hap + hass.data[HMIPC_DOMAIN] = {HAPID: mock_hap} + return mock_hap @pytest.fixture(name="simple_mock_home") diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 42ff2061698..f240d136426 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,7 +1,7 @@ """Helper for HomematicIP Cloud Tests.""" import json -from asynctest import Mock +from asynctest import Mock, patch from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, @@ -12,10 +12,15 @@ from homematicip.aio.group import AsyncGroup from homematicip.aio.home import AsyncHome from homematicip.home import Home +from homeassistant import config_entries +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.device import ( ATTR_IS_GROUP, ATTR_MODEL_TYPE, ) +from homeassistant.components.homematicip_cloud.hap import HomematicipHAP +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component from tests.common import load_fixture @@ -23,11 +28,10 @@ HAPID = "3014F7110000000000000001" HAPPIN = "5678" AUTH_TOKEN = "1234" HOME_JSON = "homematicip_cloud.json" +FIXTURE_DATA = load_fixture(HOME_JSON) -def get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model -): +def get_and_check_entity_basics(hass, mock_hap, entity_id, entity_name, device_model): """Get and test basic device.""" ha_state = hass.states.get(entity_id) assert ha_state is not None @@ -35,7 +39,7 @@ def get_and_check_entity_basics( assert ha_state.attributes[ATTR_MODEL_TYPE] == device_model assert ha_state.name == entity_name - hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id) + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) if hmip_device: if isinstance(hmip_device, AsyncDevice): @@ -67,6 +71,51 @@ async def async_manipulate_test_data( await hass.async_block_till_done() +class HomeFactory: + """Factory to create a HomematicIP Cloud Home.""" + + def __init__( + self, + hass: HomeAssistantType, + mock_connection, + hmip_config_entry: config_entries.ConfigEntry, + ): + """Initialize the Factory.""" + self.hass = hass + self.mock_connection = mock_connection + self.hmip_config_entry = hmip_config_entry + + async def async_get_mock_hap( + self, test_devices=[], test_groups=[] + ) -> HomematicipHAP: + """Create a mocked homematic access point.""" + home_name = self.hmip_config_entry.data["name"] + mock_home = ( + HomeTemplate( + connection=self.mock_connection, + home_name=home_name, + test_devices=test_devices, + test_groups=test_groups, + ) + .init_home() + .get_async_home_mock() + ) + + self.hmip_config_entry.add_to_hass(self.hass) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.get_hap", + return_value=mock_home, + ): + assert await async_setup_component(self.hass, HMIPC_DOMAIN, {}) is True + + await self.hass.async_block_till_done() + + hap = self.hass.data[HMIPC_DOMAIN][HAPID] + mock_home.on_update(hap.async_update) + mock_home.on_create(hap.async_create_entity) + return hap + + class HomeTemplate(Home): """ Home template as builder for home mock. @@ -84,17 +133,36 @@ class HomeTemplate(Home): _typeGroupMap = TYPE_GROUP_MAP _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP - def __init__(self, connection=None, home_name=""): + 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.init_json_state = None + self.test_devices = test_devices + self.test_groups = test_groups - def init_home(self, json_path=HOME_JSON): + def _cleanup_json(self, json): + if self.test_devices is not None: + new_devices = {} + for json_device in json["devices"].items(): + if json_device[1]["label"] in self.test_devices: + new_devices.update([json_device]) + json["devices"] = new_devices + + if self.test_groups is not None: + new_groups = {} + for json_group in json["groups"].items(): + if json_group[1]["label"] in self.test_groups: + new_groups.update([json_group]) + json["groups"] = new_groups + + return json + + def init_home(self): """Init template with json.""" - self.init_json_state = json.loads(load_fixture(HOME_JSON)) + self.init_json_state = self._cleanup_json(json.loads(FIXTURE_DATA)) self.update_home(json_state=self.init_json_state, clearConfig=True) return self diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index ae07b951c63..8aa101ca2f5 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -51,20 +51,23 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_alarm_control_panel(hass, default_mock_hap): +async def test_hmip_alarm_control_panel(hass, default_mock_hap_factory): """Test HomematicipAlarmControlPanel.""" entity_id = "alarm_control_panel.hmip_alarm_control_panel" entity_name = "HmIP Alarm Control Panel" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_groups=["EXTERNAL", "INTERNAL"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "disarmed" assert not hmip_device - home = default_mock_hap.home + home = mock_hap.home await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 1cfe06ff701..00e68b0c363 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -41,14 +41,17 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_acceleration_sensor(hass, default_mock_hap): +async def test_hmip_acceleration_sensor(hass, default_mock_hap_factory): """Test HomematicipAccelerationSensor.""" entity_id = "binary_sensor.garagentor" entity_name = "Garagentor" device_model = "HmIP-SAM" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -75,14 +78,17 @@ async def test_hmip_acceleration_sensor(hass, default_mock_hap): assert len(hmip_device.mock_calls) == service_call_counter + 2 -async def test_hmip_contact_interface(hass, default_mock_hap): +async def test_hmip_contact_interface(hass, default_mock_hap_factory): """Test HomematicipContactInterface.""" entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach" entity_name = "Kontakt-Schnittstelle Unterputz – 1-fach" device_model = "HmIP-FCI1" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -95,14 +101,17 @@ async def test_hmip_contact_interface(hass, default_mock_hap): assert ha_state.state == STATE_OFF -async def test_hmip_shutter_contact(hass, default_mock_hap): +async def test_hmip_shutter_contact(hass, default_mock_hap_factory): """Test HomematicipShutterContact.""" entity_id = "binary_sensor.fenstergriffsensor" entity_name = "Fenstergriffsensor" device_model = "HmIP-SRH" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -124,14 +133,17 @@ async def test_hmip_shutter_contact(hass, default_mock_hap): assert ha_state.attributes[ATTR_SABOTAGE] -async def test_hmip_motion_detector(hass, default_mock_hap): +async def test_hmip_motion_detector(hass, default_mock_hap_factory): """Test HomematicipMotionDetector.""" entity_id = "binary_sensor.bewegungsmelder_fur_55er_rahmen_innen" entity_name = "Bewegungsmelder für 55er Rahmen – innen" device_model = "HmIP-SMI55" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -140,14 +152,17 @@ async def test_hmip_motion_detector(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_presence_detector(hass, default_mock_hap): +async def test_hmip_presence_detector(hass, default_mock_hap_factory): """Test HomematicipPresenceDetector.""" entity_id = "binary_sensor.spi_1" entity_name = "SPI_1" device_model = "HmIP-SPI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -161,14 +176,19 @@ async def test_hmip_presence_detector(hass, default_mock_hap): assert ha_state.attributes[ATTR_EVENT_DELAY] -async def test_hmip_pluggable_mains_failure_surveillance_sensor(hass, default_mock_hap): +async def test_hmip_pluggable_mains_failure_surveillance_sensor( + hass, default_mock_hap_factory +): """Test HomematicipPresenceDetector.""" entity_id = "binary_sensor.netzausfall" entity_name = "Netzausfall" device_model = "HmIP-PMFS" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -177,14 +197,17 @@ async def test_hmip_pluggable_mains_failure_surveillance_sensor(hass, default_mo assert ha_state.state == STATE_OFF -async def test_hmip_smoke_detector(hass, default_mock_hap): +async def test_hmip_smoke_detector(hass, default_mock_hap_factory): """Test HomematicipSmokeDetector.""" entity_id = "binary_sensor.rauchwarnmelder" entity_name = "Rauchwarnmelder" device_model = "HmIP-SWSD" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -198,14 +221,17 @@ async def test_hmip_smoke_detector(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_water_detector(hass, default_mock_hap): +async def test_hmip_water_detector(hass, default_mock_hap_factory): """Test HomematicipWaterDetector.""" entity_id = "binary_sensor.wassersensor" entity_name = "Wassersensor" device_model = "HmIP-SWD" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -230,14 +256,17 @@ async def test_hmip_water_detector(hass, default_mock_hap): assert ha_state.state == STATE_OFF -async def test_hmip_storm_sensor(hass, default_mock_hap): +async def test_hmip_storm_sensor(hass, default_mock_hap_factory): """Test HomematicipStormSensor.""" entity_id = "binary_sensor.weather_sensor_plus_storm" entity_name = "Weather Sensor – plus Storm" device_model = "HmIP-SWO-PL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Weather Sensor – plus"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -246,14 +275,17 @@ async def test_hmip_storm_sensor(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_rain_sensor(hass, default_mock_hap): +async def test_hmip_rain_sensor(hass, default_mock_hap_factory): """Test HomematicipRainSensor.""" entity_id = "binary_sensor.wettersensor_pro_raining" entity_name = "Wettersensor - pro Raining" device_model = "HmIP-SWO-PR" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wettersensor - pro"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -262,14 +294,17 @@ async def test_hmip_rain_sensor(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_sunshine_sensor(hass, default_mock_hap): +async def test_hmip_sunshine_sensor(hass, default_mock_hap_factory): """Test HomematicipSunshineSensor.""" entity_id = "binary_sensor.wettersensor_pro_sunshine" entity_name = "Wettersensor - pro Sunshine" device_model = "HmIP-SWO-PR" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wettersensor - pro"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -279,14 +314,17 @@ async def test_hmip_sunshine_sensor(hass, default_mock_hap): assert ha_state.state == STATE_OFF -async def test_hmip_battery_sensor(hass, default_mock_hap): +async def test_hmip_battery_sensor(hass, default_mock_hap_factory): """Test HomematicipSunshineSensor.""" entity_id = "binary_sensor.wohnungsture_battery" entity_name = "Wohnungstüre Battery" device_model = "HMIP-SWDO" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wohnungstüre"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -295,14 +333,17 @@ async def test_hmip_battery_sensor(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): +async def test_hmip_security_zone_sensor_group(hass, default_mock_hap_factory): """Test HomematicipSecurityZoneSensorGroup.""" entity_id = "binary_sensor.internal_securityzone" entity_name = "INTERNAL SecurityZone" device_model = "HmIP-SecurityZone" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_groups=["INTERNAL"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -327,14 +368,15 @@ async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN -async def test_hmip_security_sensor_group(hass, default_mock_hap): +async def test_hmip_security_sensor_group(hass, default_mock_hap_factory): """Test HomematicipSecuritySensorGroup.""" entity_id = "binary_sensor.buro_sensors" entity_name = "Büro Sensors" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Büro"]) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) await async_manipulate_test_data( diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index db052929474..d0bdaf85c00 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -41,14 +41,18 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_heating_group_heat(hass, default_mock_hap): +async def test_hmip_heating_group_heat(hass, default_mock_hap_factory): """Test HomematicipHeatingGroup.""" entity_id = "climate.badezimmer" entity_name = "Badezimmer" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wandthermostat", "Heizkörperthermostat"], + test_groups=[entity_name], + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == HVAC_MODE_AUTO @@ -142,7 +146,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") await async_manipulate_test_data( hass, - default_mock_hap.home.get_functionalHome(IndoorClimateHome), + mock_hap.home.get_functionalHome(IndoorClimateHome), "absenceType", AbsenceType.VACATION, fire_device=hmip_device, @@ -153,7 +157,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") await async_manipulate_test_data( hass, - default_mock_hap.home.get_functionalHome(IndoorClimateHome), + mock_hap.home.get_functionalHome(IndoorClimateHome), "absenceType", AbsenceType.PERIOD, fire_device=hmip_device, @@ -172,7 +176,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - default_mock_hap.home.get_functionalHome( + mock_hap.home.get_functionalHome( IndoorClimateHome ).absenceType = AbsenceType.PERMANENT await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") @@ -230,14 +234,17 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE -async def test_hmip_heating_group_cool(hass, default_mock_hap): +async def test_hmip_heating_group_cool(hass, default_mock_hap_factory): """Test HomematicipHeatingGroup.""" entity_id = "climate.badezimmer" entity_name = "Badezimmer" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_groups=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) hmip_device.activeProfile = hmip_device.profiles[3] @@ -347,14 +354,17 @@ async def test_hmip_heating_group_cool(hass, default_mock_hap): assert hmip_device.mock_calls[-1][1] == (4,) -async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap): +async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap_factory): """Test HomematicipHeatingGroup.""" entity_id = "climate.schlafzimmer" entity_name = "Schlafzimmer" device_model = None - + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wandthermostat", "Heizkörperthermostat", "Pc"], + test_groups=[entity_name], + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert hmip_device @@ -480,14 +490,17 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access -async def test_hmip_heating_group_services(hass, mock_hap_with_service): +async def test_hmip_heating_group_services(hass, default_mock_hap_factory): """Test HomematicipHeatingGroup services.""" entity_id = "climate.badezimmer" entity_name = "Badezimmer" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_groups=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, mock_hap_with_service, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state @@ -512,6 +525,5 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) assert ( - len(hmip_device._connection.mock_calls) # pylint: disable=protected-access - == 12 + len(hmip_device._connection.mock_calls) == 4 # pylint: disable=protected-access ) diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 5b267628ae3..000e859c08e 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -24,14 +24,17 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_cover_shutter(hass, default_mock_hap): +async def test_hmip_cover_shutter(hass, default_mock_hap_factory): """Test HomematicipCoverShutte.""" entity_id = "cover.sofa_links" entity_name = "Sofa links" device_model = "HmIP-FBL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "closed" @@ -90,14 +93,17 @@ async def test_hmip_cover_shutter(hass, default_mock_hap): assert ha_state.state == STATE_UNKNOWN -async def test_hmip_cover_slats(hass, default_mock_hap): +async def test_hmip_cover_slats(hass, default_mock_hap_factory): """Test HomematicipCoverSlats.""" entity_id = "cover.sofa_links" entity_name = "Sofa links" device_model = "HmIP-FBL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_CLOSED @@ -157,14 +163,17 @@ async def test_hmip_cover_slats(hass, default_mock_hap): assert ha_state.state == STATE_UNKNOWN -async def test_hmip_garage_door_tormatic(hass, default_mock_hap): +async def test_hmip_garage_door_tormatic(hass, default_mock_hap_factory): """Test HomematicipCoverShutte.""" entity_id = "cover.garage_door_module" entity_name = "Garage Door Module" device_model = "HmIP-MOD-TM" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "closed" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 4ce6283d64d..4334a049564 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -7,18 +7,25 @@ from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er -from .conftest import get_mock_hap -from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics +from .helper import ( + HAPID, + HomeFactory, + async_manipulate_test_data, + get_and_check_entity_basics, +) -async def test_hmip_remove_device(hass, default_mock_hap): +async def test_hmip_remove_device(hass, default_mock_hap_factory): """Test Remove of hmip device.""" entity_id = "light.treppe" entity_name = "Treppe" device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -29,7 +36,7 @@ async def test_hmip_remove_device(hass, default_mock_hap): pre_device_count = len(device_registry.devices) pre_entity_count = len(entity_registry.entities) - pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + pre_mapping_count = len(mock_hap.hmip_device_by_entity_id) hmip_device.fire_remove_event() @@ -37,17 +44,20 @@ async def test_hmip_remove_device(hass, default_mock_hap): assert len(device_registry.devices) == pre_device_count - 1 assert len(entity_registry.entities) == pre_entity_count - 3 - assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 + assert len(mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 -async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry): +async def test_hmip_add_device(hass, default_mock_hap_factory, hmip_config_entry): """Test Remove of hmip device.""" entity_id = "light.treppe" entity_name = "Treppe" device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -58,25 +68,25 @@ async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry): pre_device_count = len(device_registry.devices) pre_entity_count = len(entity_registry.entities) - pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + pre_mapping_count = len(mock_hap.hmip_device_by_entity_id) hmip_device.fire_remove_event() await hass.async_block_till_done() assert len(device_registry.devices) == pre_device_count - 1 assert len(entity_registry.entities) == pre_entity_count - 3 - assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 + assert len(mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 reloaded_hap = HomematicipHAP(hass, hmip_config_entry) with patch( "homeassistant.components.homematicip_cloud.HomematicipHAP", return_value=reloaded_hap, ), patch.object(reloaded_hap, "async_connect"), patch.object( - reloaded_hap, "get_hap", return_value=default_mock_hap.home + reloaded_hap, "get_hap", return_value=mock_hap.home ), patch( "homeassistant.components.homematicip_cloud.hap.asyncio.sleep" ): - default_mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED) + mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED) await hass.async_block_till_done() assert len(device_registry.devices) == pre_device_count @@ -85,14 +95,15 @@ async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry): assert len(new_hap.hmip_device_by_entity_id) == pre_mapping_count -async def test_hmip_remove_group(hass, default_mock_hap): +async def test_hmip_remove_group(hass, default_mock_hap_factory): """Test Remove of hmip group.""" entity_id = "switch.strom_group" entity_name = "Strom Group" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Strom"]) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -103,59 +114,67 @@ async def test_hmip_remove_group(hass, default_mock_hap): pre_device_count = len(device_registry.devices) pre_entity_count = len(entity_registry.entities) - pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + pre_mapping_count = len(mock_hap.hmip_device_by_entity_id) hmip_device.fire_remove_event() await hass.async_block_till_done() assert len(device_registry.devices) == pre_device_count assert len(entity_registry.entities) == pre_entity_count - 1 - assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 1 + assert len(mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 1 -async def test_all_devices_unavailable_when_hap_not_connected(hass, default_mock_hap): +async def test_all_devices_unavailable_when_hap_not_connected( + hass, default_mock_hap_factory +): """Test make all devices unavaulable when hap is not connected.""" entity_id = "light.treppe" entity_name = "Treppe" device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON assert hmip_device - assert default_mock_hap.home.connected + assert mock_hap.home.connected - await async_manipulate_test_data(hass, default_mock_hap.home, "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_UNAVAILABLE -async def test_hap_reconnected(hass, default_mock_hap): +async def test_hap_reconnected(hass, default_mock_hap_factory): """Test reconnect hap.""" entity_id = "light.treppe" entity_name = "Treppe" device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON assert hmip_device - assert default_mock_hap.home.connected + assert mock_hap.home.connected - await async_manipulate_test_data(hass, default_mock_hap.home, "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_UNAVAILABLE - default_mock_hap._accesspoint_connected = False # pylint: disable=protected-access - await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True) + mock_hap._accesspoint_connected = False # pylint: disable=protected-access + await async_manipulate_test_data(hass, mock_hap.home, "connected", True) await hass.async_block_till_done() ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -169,7 +188,9 @@ async def test_hap_with_name(hass, mock_connection, hmip_config_entry): device_model = "HmIP-BSL" hmip_config_entry.data["name"] = home_name - mock_hap = await get_mock_hap(hass, mock_connection, hmip_config_entry) + mock_hap = await HomeFactory( + hass, mock_connection, hmip_config_entry + ).async_get_mock_hap(test_devices=["Treppe"]) assert mock_hap ha_state, hmip_device = get_and_check_entity_basics( @@ -181,14 +202,17 @@ async def test_hap_with_name(hass, mock_connection, hmip_config_entry): assert ha_state.attributes["friendly_name"] == entity_name -async def test_hmip_reset_energy_counter_services(hass, mock_hap_with_service): +async def test_hmip_reset_energy_counter_services(hass, default_mock_hap_factory): """Test reset_energy_counter service.""" entity_id = "switch.pc" entity_name = "Pc" device_model = "HMIP-PSM" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, mock_hap_with_service, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state @@ -205,4 +229,4 @@ async def test_hmip_reset_energy_counter_services(hass, mock_hap_with_service): "homematicip_cloud", "reset_energy_counter", {"entity_id": "all"}, blocking=True ) assert hmip_device.mock_calls[-1][0] == "reset_energy_counter" - assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 + assert len(hmip_device._connection.mock_calls) == 4 # pylint: disable=W0212 diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index e42dfe8fb4e..ae41bec1218 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -107,9 +107,10 @@ async def test_hap_setup_connection_error(): assert not hass.config_entries.flow.async_init.mock_calls -async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap): +async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap_factory): """Test calling reset while the entry has been setup.""" - assert hass.data[HMIPC_DOMAIN][HAPID] == default_mock_hap + mock_hap = await default_mock_hap_factory.async_get_mock_hap() + assert hass.data[HMIPC_DOMAIN][HAPID] == mock_hap config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 # hap_reset is called during unload diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 632a6aac449..31919c5e84a 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -28,14 +28,17 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_light(hass, default_mock_hap): +async def test_hmip_light(hass, default_mock_hap_factory): """Test HomematicipLight.""" entity_id = "light.treppe" entity_name = "Treppe" device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -64,14 +67,17 @@ async def test_hmip_light(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_notification_light(hass, default_mock_hap): +async def test_hmip_notification_light(hass, default_mock_hap_factory): """Test HomematicipNotificationLight.""" entity_id = "light.treppe_top_notification" entity_name = "Treppe Top Notification" device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Treppe"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -152,14 +158,17 @@ async def test_hmip_notification_light(hass, default_mock_hap): assert not ha_state.attributes.get(ATTR_BRIGHTNESS) -async def test_hmip_dimmer(hass, default_mock_hap): +async def test_hmip_dimmer(hass, default_mock_hap_factory): """Test HomematicipDimmer.""" entity_id = "light.schlafzimmerlicht" entity_name = "Schlafzimmerlicht" device_model = "HmIP-BDT" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF @@ -201,14 +210,17 @@ async def test_hmip_dimmer(hass, default_mock_hap): assert not ha_state.attributes.get(ATTR_BRIGHTNESS) -async def test_hmip_light_measuring(hass, default_mock_hap): +async def test_hmip_light_measuring(hass, default_mock_hap_factory): """Test HomematicipLightMeasuring.""" entity_id = "light.flur_oben" entity_name = "Flur oben" device_model = "HmIP-BSM" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index f0a81c69074..46d26a3a8df 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -39,14 +39,17 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_accesspoint_status(hass, default_mock_hap): +async def test_hmip_accesspoint_status(hass, default_mock_hap_factory): """Test HomematicipSwitch.""" entity_id = "sensor.access_point" entity_name = "Access Point" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert hmip_device assert ha_state.state == "8.0" @@ -58,14 +61,17 @@ async def test_hmip_accesspoint_status(hass, default_mock_hap): assert ha_state.state == "17.3" -async def test_hmip_heating_thermostat(hass, default_mock_hap): +async def test_hmip_heating_thermostat(hass, default_mock_hap_factory): """Test HomematicipHeatingThermostat.""" entity_id = "sensor.heizkorperthermostat_heating" entity_name = "Heizkörperthermostat Heating" device_model = "HMIP-eTRV" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "0" @@ -89,14 +95,17 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap): assert ha_state.attributes["icon"] == "mdi:battery-outline" -async def test_hmip_humidity_sensor(hass, default_mock_hap): +async def test_hmip_humidity_sensor(hass, default_mock_hap_factory): """Test HomematicipHumiditySensor.""" entity_id = "sensor.bwth_1_humidity" entity_name = "BWTH 1 Humidity" device_model = "HmIP-BWTH" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["BWTH 1"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "40" @@ -109,14 +118,17 @@ async def test_hmip_humidity_sensor(hass, default_mock_hap): assert ha_state.attributes[ATTR_RSSI_PEER] == -77 -async def test_hmip_temperature_sensor1(hass, default_mock_hap): +async def test_hmip_temperature_sensor1(hass, default_mock_hap_factory): """Test HomematicipTemperatureSensor.""" entity_id = "sensor.bwth_1_temperature" entity_name = "BWTH 1 Temperature" device_model = "HmIP-BWTH" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["BWTH 1"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "21.0" @@ -131,14 +143,17 @@ async def test_hmip_temperature_sensor1(hass, default_mock_hap): assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 -async def test_hmip_temperature_sensor2(hass, default_mock_hap): +async def test_hmip_temperature_sensor2(hass, default_mock_hap_factory): """Test HomematicipTemperatureSensor.""" entity_id = "sensor.heizkorperthermostat_temperature" entity_name = "Heizkörperthermostat Temperature" device_model = "HMIP-eTRV" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "20.0" @@ -153,14 +168,17 @@ async def test_hmip_temperature_sensor2(hass, default_mock_hap): assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 -async def test_hmip_power_sensor(hass, default_mock_hap): +async def test_hmip_power_sensor(hass, default_mock_hap_factory): """Test HomematicipPowerSensor.""" entity_id = "sensor.flur_oben_power" entity_name = "Flur oben Power" device_model = "HmIP-BSM" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Flur oben"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "0.0" @@ -187,14 +205,17 @@ async def test_hmip_power_sensor(hass, default_mock_hap): assert ha_state.attributes[ATTR_CONFIG_PENDING] -async def test_hmip_illuminance_sensor1(hass, default_mock_hap): +async def test_hmip_illuminance_sensor1(hass, default_mock_hap_factory): """Test HomematicipIlluminanceSensor.""" entity_id = "sensor.wettersensor_illuminance" entity_name = "Wettersensor Illuminance" device_model = "HmIP-SWO-B" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wettersensor"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "4890.0" @@ -204,14 +225,17 @@ async def test_hmip_illuminance_sensor1(hass, default_mock_hap): assert ha_state.state == "231" -async def test_hmip_illuminance_sensor2(hass, default_mock_hap): +async def test_hmip_illuminance_sensor2(hass, default_mock_hap_factory): """Test HomematicipIlluminanceSensor.""" entity_id = "sensor.lichtsensor_nord_illuminance" entity_name = "Lichtsensor Nord Illuminance" device_model = "HmIP-SLO" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Lichtsensor Nord"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "807.3" @@ -224,14 +248,17 @@ async def test_hmip_illuminance_sensor2(hass, default_mock_hap): assert ha_state.attributes[ATTR_LOWEST_ILLUMINATION] == 785.2 -async def test_hmip_windspeed_sensor(hass, default_mock_hap): +async def test_hmip_windspeed_sensor(hass, default_mock_hap_factory): """Test HomematicipWindspeedSensor.""" entity_id = "sensor.wettersensor_pro_windspeed" entity_name = "Wettersensor - pro Windspeed" device_model = "HmIP-SWO-PR" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wettersensor - pro"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "2.6" @@ -268,14 +295,17 @@ async def test_hmip_windspeed_sensor(hass, default_mock_hap): assert ha_state.attributes[ATTR_WIND_DIRECTION] == txt -async def test_hmip_today_rain_sensor(hass, default_mock_hap): +async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): """Test HomematicipTodayRainSensor.""" entity_id = "sensor.weather_sensor_plus_today_rain" entity_name = "Weather Sensor – plus Today Rain" device_model = "HmIP-SWO-PL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Weather Sensor – plus"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "3.9" @@ -285,14 +315,17 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap): assert ha_state.state == "14.2" -async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap): +async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap_factory): """Test HomematicipPassageDetectorDeltaCounter.""" entity_id = "sensor.spdr_1" entity_name = "SPDR_1" device_model = "HmIP-SPDR" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "164" diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index b8ca7b4b67e..245d407d103 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -25,14 +25,17 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_switch(hass, default_mock_hap): +async def test_hmip_switch(hass, default_mock_hap_factory): """Test HomematicipSwitch.""" entity_id = "switch.schrank" entity_name = "Schrank" device_model = "HMIP-PS" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -59,14 +62,17 @@ async def test_hmip_switch(hass, default_mock_hap): assert ha_state.state == STATE_ON -async def test_hmip_switch_measuring(hass, default_mock_hap): +async def test_hmip_switch_measuring(hass, default_mock_hap_factory): """Test HomematicipSwitchMeasuring.""" entity_id = "switch.pc" entity_name = "Pc" device_model = "HMIP-PSM" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -100,14 +106,15 @@ async def test_hmip_switch_measuring(hass, default_mock_hap): assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH) -async def test_hmip_group_switch(hass, default_mock_hap): +async def test_hmip_group_switch(hass, default_mock_hap_factory): """Test HomematicipGroupSwitch.""" entity_id = "switch.strom_group" entity_name = "Strom Group" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_groups=["Strom"]) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_ON @@ -139,14 +146,17 @@ async def test_hmip_group_switch(hass, default_mock_hap): assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] -async def test_hmip_multi_switch(hass, default_mock_hap): +async def test_hmip_multi_switch(hass, default_mock_hap_factory): """Test HomematicipMultiSwitch.""" entity_id = "switch.jalousien_1_kizi_2_schlazi_channel1" entity_name = "Jalousien - 1 KiZi, 2 SchlaZi Channel1" device_model = "HmIP-PCBS2" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Jalousien - 1 KiZi, 2 SchlaZi"] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py index 9427a2d05bf..3f755ecee2b 100644 --- a/tests/components/homematicip_cloud/test_weather.py +++ b/tests/components/homematicip_cloud/test_weather.py @@ -24,14 +24,17 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_weather_sensor(hass, default_mock_hap): +async def test_hmip_weather_sensor(hass, default_mock_hap_factory): """Test HomematicipWeatherSensor.""" entity_id = "weather.weather_sensor_plus" entity_name = "Weather Sensor – plus" device_model = "HmIP-SWO-PL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "" @@ -45,14 +48,17 @@ async def test_hmip_weather_sensor(hass, default_mock_hap): assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 -async def test_hmip_weather_sensor_pro(hass, default_mock_hap): +async def test_hmip_weather_sensor_pro(hass, default_mock_hap_factory): """Test HomematicipWeatherSensorPro.""" entity_id = "weather.wettersensor_pro" entity_name = "Wettersensor - pro" device_model = "HmIP-SWO-PR" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert ha_state.state == "sunny" @@ -67,14 +73,15 @@ async def test_hmip_weather_sensor_pro(hass, default_mock_hap): assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 -async def test_hmip_home_weather(hass, default_mock_hap): +async def test_hmip_home_weather(hass, default_mock_hap_factory): """Test HomematicipHomeWeather.""" entity_id = "weather.weather_1010_wien_osterreich" entity_name = "Weather 1010 Wien, Österreich" device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap() ha_state, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model + hass, mock_hap, entity_id, entity_name, device_model ) assert hmip_device assert ha_state.state == "partlycloudy" @@ -85,11 +92,7 @@ async def test_hmip_home_weather(hass, default_mock_hap): assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" await async_manipulate_test_data( - hass, - default_mock_hap.home.weather, - "temperature", - 28.3, - fire_device=default_mock_hap.home, + hass, mock_hap.home.weather, "temperature", 28.3, fire_device=mock_hap.home ) ha_state = hass.states.get(entity_id) From e6148d223a1722fe6e01ebce8c522672387d8559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 14 Feb 2020 17:04:41 +0000 Subject: [PATCH 249/378] Allow hourly forecast in IPMA (#30979) * update ipma component for pyipma 2.0 * fix wind speed; refactor forecast * update requirements*.txt * fix tests; update CODEOWNERS; update pyipma to 2.0.1 * minor changes as suggested in PR * make lint happy * fix mocking coroutines * restore old unique id * fix station lat/lon; update pyipma version * add hourly forecast option to IPMA * add forecast tests * use for instead of lambda --- homeassistant/components/ipma/config_flow.py | 4 +- homeassistant/components/ipma/strings.json | 3 +- homeassistant/components/ipma/weather.py | 88 +++++++++++++++----- tests/components/ipma/test_weather.py | 69 ++++++++++++++- 4 files changed, 139 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index d1532066f68..3811d30bfbe 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -2,10 +2,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME import homeassistant.helpers.config_validation as cv from .const import DOMAIN, HOME_LOCATION_NAME +from .weather import FORECAST_MODE @config_entries.HANDLERS.register(DOMAIN) @@ -49,6 +50,7 @@ class IpmaFlowHandler(config_entries.ConfigFlow): vol.Required(CONF_NAME, default=name): str, vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude, + vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ), errors=self._errors, diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index f22d1b62fe4..ea8b9edcc86 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -8,7 +8,8 @@ "data": { "name": "Name", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "mode": "Mode" } } }, diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 7b07406d007..1fce3922b58 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -13,13 +13,22 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + TEMP_CELSIUS, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle +from homeassistant.util.dt import now, parse_datetime _LOGGER = logging.getLogger(__name__) @@ -44,11 +53,14 @@ CONDITION_CLASSES = { "exceptional": [], } +FORECAST_MODE = ["hourly", "daily"] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ) @@ -96,10 +108,12 @@ async def async_get_location(hass, api, latitude, longitude): location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( - "Initializing for coordinates %s, %s -> station %s", + "Initializing for coordinates %s, %s -> station %s (%d, %d)", latitude, longitude, location.station, + location.id_station, + location.global_id_local, ) return location @@ -112,6 +126,7 @@ class IPMAWeather(WeatherEntity): """Initialise the platform with a data instance and station name.""" self._api = api self._location_name = config.get(CONF_NAME, location.name) + self._mode = config.get(CONF_MODE) self._location = location self._observation = None self._forecast = None @@ -129,7 +144,7 @@ class IPMAWeather(WeatherEntity): _LOGGER.warning("Could not update weather observation") if new_forecast: - self._forecast = [f for f in new_forecast if f.forecasted_hours == 24] + self._forecast = new_forecast else: _LOGGER.warning("Could not update weather forecast") @@ -220,22 +235,57 @@ class IPMAWeather(WeatherEntity): if not self._forecast: return [] - fcdata_out = [ - { - ATTR_FORECAST_TIME: data_in.forecast_date, - ATTR_FORECAST_CONDITION: next( - ( - k - for k, v in CONDITION_CLASSES.items() - if int(data_in.weather_type) in v + if self._mode == "hourly": + forecast_filtered = [ + x + for x in self._forecast + if x.forecasted_hours == 1 + and parse_datetime(x.forecast_date) + > (now().utcnow() - timedelta(hours=1)) + ] + + fcdata_out = [ + { + ATTR_FORECAST_TIME: data_in.forecast_date, + ATTR_FORECAST_CONDITION: next( + ( + k + for k, v in CONDITION_CLASSES.items() + if int(data_in.weather_type) in v + ), + None, ), - None, - ), - ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_TEMP: data_in.max_temperature, - ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, - } - for data_in in self._forecast - ] + ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), + ATTR_FORECAST_PRECIPITATION: ( + data_in.precipitation_probability + if float(data_in.precipitation_probability) >= 0 + else None + ), + ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, + } + for data_in in forecast_filtered + ] + else: + forecast_filtered = [f for f in self._forecast if f.forecasted_hours == 24] + fcdata_out = [ + { + ATTR_FORECAST_TIME: data_in.forecast_date, + ATTR_FORECAST_CONDITION: next( + ( + k + for k, v in CONDITION_CLASSES.items() + if int(data_in.weather_type) in v + ), + None, + ), + ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, + ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, + } + for data_in in forecast_filtered + ] return fcdata_out diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index ead4654cba2..7a6e1160f24 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -4,6 +4,14 @@ from unittest.mock import patch from homeassistant.components import weather from homeassistant.components.weather import ( + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -12,6 +20,7 @@ from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, ) from homeassistant.setup import async_setup_component +from homeassistant.util.dt import now from tests.common import MockConfigEntry, mock_coro @@ -71,16 +80,16 @@ class MockLocation: "2020-01-15T07:51:00", 9, "S", - None, + "10", ), Forecast( "7.7", - "2020-01-15T02:00:00", + now().utcnow().strftime("%Y-%m-%dT%H:%M:%S"), 1, "86.9", None, None, - "-99.0", + "80.0", 10.6, "2020-01-15T07:51:00", 10, @@ -122,7 +131,9 @@ async def test_setup_configuration(hass): return_value=mock_coro(MockLocation()), ): assert await async_setup_component( - hass, weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma"}} + hass, + weather.DOMAIN, + {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}}, ) await hass.async_block_till_done() @@ -158,3 +169,53 @@ async def test_setup_config_flow(hass): assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94 assert data.get(ATTR_WEATHER_WIND_BEARING) == "NW" assert state.attributes.get("friendly_name") == "HomeTown" + + +async def test_daily_forecast(hass): + """Test for successfully getting daily forecast.""" + with patch( + "homeassistant.components.ipma.weather.async_get_location", + return_value=mock_coro(MockLocation()), + ): + assert await async_setup_component( + hass, + weather.DOMAIN, + {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "daily"}}, + ) + await hass.async_block_till_done() + + state = hass.states.get("weather.hometown") + assert state.state == "rainy" + + forecast = state.attributes.get(ATTR_FORECAST)[0] + assert forecast.get(ATTR_FORECAST_TIME) == "2020-01-15T00:00:00" + assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" + assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 + assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 + assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "100.0" + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "10" + assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" + + +async def test_hourly_forecast(hass): + """Test for successfully getting daily forecast.""" + with patch( + "homeassistant.components.ipma.weather.async_get_location", + return_value=mock_coro(MockLocation()), + ): + assert await async_setup_component( + hass, + weather.DOMAIN, + {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}}, + ) + await hass.async_block_till_done() + + state = hass.states.get("weather.hometown") + assert state.state == "rainy" + + forecast = state.attributes.get(ATTR_FORECAST)[0] + assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" + assert forecast.get(ATTR_FORECAST_TEMP) == 7.7 + assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "80.0" + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "32.7" + assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" From 71a81c443f1523e68f90aaf5e1e48ed2d347f89a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Feb 2020 09:26:50 -0800 Subject: [PATCH 250/378] Limit translations_develop to an integration (#31804) * limit translations_develop to english * Convert to Python * Limit to integration * Add to hassfest * Remove old bash comment --- script/scaffold/__main__.py | 6 +++ script/translations_develop | 86 +++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 8fa2814e54f..d3b68914104 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -82,6 +82,12 @@ def main(): subprocess.run(["python", "-m", "script.gen_requirements_all"], **pipe_null) print() + print("Running script/translations_develop to pick up new translation strings.") + subprocess.run( + ["script/translations_develop", "--integration", info.domain], **pipe_null + ) + print() + if args.develop: print("Running tests") print(f"$ pytest -vvv tests/components/{info.domain}") diff --git a/script/translations_develop b/script/translations_develop index eb9d685fa8e..c3bf8d8e03f 100755 --- a/script/translations_develop +++ b/script/translations_develop @@ -1,21 +1,81 @@ -#!/usr/bin/env bash +#!/usr/bin/env python # Compile the current translation strings files for testing -# Safe bash settings -# -e Exit on command fail -# -u Exit on unset variable -# -o pipefail Exit if piped command has error code -set -eu -o pipefail +import argparse +import json +import os +from pathlib import Path +from shutil import rmtree +import subprocess +import sys -cd "$(dirname "$0")/.." -mkdir -p build/translations-download +def valid_integration(integration): + """Test if it's a valid integration.""" + if not Path(f"homeassistant/components/{integration}").exists(): + raise argparse.ArgumentTypeError( + f"The integration {integration} does not exist." + ) -script/translations_upload_merge.py + return integration -# Use the generated translations upload file as the mock output from the -# Lokalise download -mv build/translations-upload.json build/translations-download/en.json -script/translations_download_split.py +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = argparse.ArgumentParser(description="Develop Translations") + parser.add_argument( + "--integration", type=valid_integration, help="Integration to process." + ) + + arguments = parser.parse_args() + + return arguments + + +def main(): + """Run the script.""" + if not os.path.isfile("requirements_all.txt"): + print("Run this from HA root dir") + return + + args = get_arguments() + if args.integration: + integration = args.integration + else: + integration = None + while ( + integration is None + or not Path(f"homeassistant/components/{integration}").exists() + ): + if integration is not None: + print(f"Integration {integration} doesn't exist!") + print() + integration = input("Integration to process: ") + + download_dir = Path("build/translations-download") + + if download_dir.is_dir(): + rmtree(str(download_dir)) + + download_dir.mkdir() + + subprocess.run("script/translations_upload_merge.py") + + raw_data = json.loads(Path("build/translations-upload.json").read_text()) + + if integration not in raw_data["component"]: + print("Integration has no strings.json") + sys.exit(1) + + Path("build/translations-download/en.json").write_text( + json.dumps({"component": {integration: raw_data["component"][integration]}}) + ) + + subprocess.run( + ["script/translations_download_split.py", "--integration", "{integration}"] + ) + + +if __name__ == "__main__": + main() From e019280d94261688795a5d32c9a1a7a5e7948836 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Feb 2020 10:00:22 -0800 Subject: [PATCH 251/378] Annotate more async functions correctly (#31802) --- homeassistant/components/coolmaster/config_flow.py | 1 + homeassistant/components/esphome/config_flow.py | 3 ++- homeassistant/components/homekit_controller/config_flow.py | 1 + homeassistant/components/mysensors/light.py | 4 ++++ homeassistant/components/sonos/media_player.py | 1 + homeassistant/components/starline/config_flow.py | 6 +++++- homeassistant/components/upnp/sensor.py | 2 +- homeassistant/config_entries.py | 1 + homeassistant/helpers/entity_component.py | 1 + homeassistant/helpers/restore_state.py | 1 + 10 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index e9cef562647..c267b283118 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -26,6 +26,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @core.callback def _async_get_entry(self, data): supported_modes = [ key for (key, value) in data.items() if key in AVAILABLE_MODES and value diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 53289799b43..1085f734f17 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -5,7 +5,7 @@ from typing import Optional from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, core from homeassistant.helpers import ConfigType from .entry_data import DATA_KEY, RuntimeEntryData @@ -115,6 +115,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): return await self.async_step_discovery_confirm() + @core.callback def _async_get_entry(self): return self.async_create_entry( title=self._name, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 7560f0d3a5d..559e0b4a997 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -352,6 +352,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return self._async_step_pair_show_form(errors) + @callback def _async_step_pair_show_form(self, errors=None): return self.async_show_form( step_id="pair", diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 19eb8e9e92c..45da4a77d5f 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -11,6 +11,7 @@ from homeassistant.components.light import ( Light, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list @@ -150,11 +151,13 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): self._values[value_type] = STATE_OFF self.async_schedule_update_ha_state() + @callback def _async_update_light(self): """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT self._state = self._values[value_type] == STATE_ON + @callback def _async_update_dimmer(self): """Update the controller with values from dimmer child.""" value_type = self.gateway.const.SetReq.V_DIMMER @@ -163,6 +166,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): if self._brightness == 0: self._state = False + @callback def _async_update_rgb_or_w(self): """Update the controller with values from RGB or RGBW child.""" value = self._values[self.value_type] diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 84920b6c94d..37b479a90b1 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -762,6 +762,7 @@ class SonosEntity(MediaPlayerDevice): return await self.hass.async_add_executor_job(_get_soco_group) + @callback def _async_regroup(group): """Rebuild internal group layout.""" sonos_group = [] diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index fa559f62913..34415e9dca4 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional from starline import StarlineAuth import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import ( # pylint: disable=unused-import @@ -85,6 +85,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_authenticate_user(error) return self._async_form_auth_captcha(error) + @core.callback def _async_form_auth_app(self, error=None): """Authenticate application form.""" errors = {} @@ -106,6 +107,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + @core.callback def _async_form_auth_user(self, error=None): """Authenticate user form.""" errors = {} @@ -127,6 +129,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + @core.callback def _async_form_auth_mfa(self, error=None): """Authenticate mfa form.""" errors = {} @@ -146,6 +149,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"phone_number": self._phone_number}, ) + @core.callback def _async_form_auth_captcha(self, error=None): """Captcha verification form.""" errors = {} diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 81fd5c025b9..db121678d93 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -170,7 +170,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Get unit we are measuring in.""" raise NotImplementedError() - def _async_fetch_value(self): + async def _async_fetch_value(self): """Fetch a value from the IGD.""" raise NotImplementedError() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 35631225bfb..1cec1e75fe9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -775,6 +775,7 @@ class ConfigEntries: return await entry.async_unload(self.hass, integration=integration) + @callback def _async_schedule_save(self) -> None: """Save the entity registry to a file.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index e26dc5dfbea..f6c473dd418 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -273,6 +273,7 @@ class EntityComponent: return processed_conf + @callback def _async_init_entity_platform( self, platform_type: str, diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index e8f0f9a6bac..7b2ec8c7f75 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -174,6 +174,7 @@ class RestoreStateData: def async_setup_dump(self, *args: Any) -> None: """Set up the restore state listeners.""" + @callback def _async_dump_states(*_: Any) -> None: self.hass.async_create_task(self.async_dump_states()) From d6f0c26e7f84bb2a0e05631ce18c4ab8e318fe52 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 14 Feb 2020 11:24:35 -0700 Subject: [PATCH 252/378] Fire HASS events on SimpliSafe events (#31811) * Fire HASS events on SimpliSafe events * Bump simplisafe-ptyhon to 7.3.0 * Update reqirements * Updates * Revert "Updates" This reverts commit 55818894176fe27938c79685ab87100d1bc120ff. * Restrict which events get fired * Code review comments --- .../components/simplisafe/__init__.py | 43 ++++++++++++------- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 74c7c3fd079..d655e149469 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,6 +1,6 @@ """Support for SimpliSafe alarm systems.""" import asyncio -from dataclasses import dataclass, field +from dataclasses import InitVar, asdict, dataclass, field from datetime import datetime import logging from typing import Optional @@ -9,8 +9,12 @@ from simplipy import API from simplipy.entity import EntityTypes from simplipy.errors import InvalidCredentialsError, SimplipyError, WebsocketError from simplipy.websocket import ( + EVENT_CAMERA_MOTION_DETECTED, + EVENT_DOORBELL_DETECTED, + EVENT_ENTRY_DETECTED, EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, + EVENT_MOTION_DETECTED, get_event_type_from_payload, ) import voluptuous as vol @@ -60,10 +64,18 @@ CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" TOPIC_UPDATE = "simplisafe_update_data_{0}" +EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" + DEFAULT_SOCKET_MIN_RETRY = 15 DEFAULT_WATCHDOG_SECONDS = 5 * 60 WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] +WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [ + EVENT_CAMERA_MOTION_DETECTED, + EVENT_DOORBELL_DETECTED, + EVENT_ENTRY_DETECTED, + EVENT_MOTION_DETECTED, +] ATTR_LAST_EVENT_INFO = "last_event_info" ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" @@ -309,7 +321,7 @@ async def async_unload_entry(hass, entry): class SimpliSafeWebsocketEvent: """Define a representation of a parsed websocket event.""" - event_data: dict + event_data: InitVar[dict] changed_by: Optional[str] = field(init=False) event_type: Optional[str] = field(init=False) @@ -320,30 +332,28 @@ class SimpliSafeWebsocketEvent: system_id: int = field(init=False) timestamp: datetime = field(init=False) - def __post_init__(self): + def __post_init__(self, event_data): """Initialize.""" - object.__setattr__(self, "changed_by", self.event_data["pinName"]) - object.__setattr__( - self, "event_type", get_event_type_from_payload(self.event_data) - ) - object.__setattr__(self, "info", self.event_data["info"]) - object.__setattr__(self, "sensor_name", self.event_data["sensorName"]) - object.__setattr__(self, "sensor_serial", self.event_data["sensorSerial"]) + object.__setattr__(self, "changed_by", event_data["pinName"]) + object.__setattr__(self, "event_type", get_event_type_from_payload(event_data)) + object.__setattr__(self, "info", event_data["info"]) + object.__setattr__(self, "sensor_name", event_data["sensorName"]) + object.__setattr__(self, "sensor_serial", event_data["sensorSerial"]) try: object.__setattr__( - self, "sensor_type", EntityTypes(self.event_data["sensorType"]).name + self, "sensor_type", EntityTypes(event_data["sensorType"]).name ) except ValueError: _LOGGER.warning( 'Encountered unknown entity type: %s ("%s"). Please report it at' "https://github.com/home-assistant/home-assistant/issues.", - self.event_data["sensorType"], - self.event_data["sensorName"], + event_data["sensorType"], + event_data["sensorName"], ) object.__setattr__(self, "sensor_type", None) - object.__setattr__(self, "system_id", self.event_data["sid"]) + object.__setattr__(self, "system_id", event_data["sid"]) object.__setattr__( - self, "timestamp", utc_from_timestamp(self.event_data["eventTimestamp"]) + self, "timestamp", utc_from_timestamp(event_data["eventTimestamp"]) ) @@ -407,6 +417,9 @@ class SimpliSafeWebsocket: self.last_events[data["sid"]] = event async_dispatcher_send(self._hass, TOPIC_UPDATE.format(data["sid"])) + if event.event_type in WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT: + self._hass.bus.async_fire(EVENT_SIMPLISAFE_EVENT, event_data=asdict(event)) + _LOGGER.debug("Resetting websocket watchdog") self._websocket_watchdog_listener() self._websocket_watchdog_listener = async_call_later( diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 3b04d26732c..b150fd40392 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==7.1.0"], + "requirements": ["simplisafe-python==7.3.0"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 39b1194cce0..cb9d5f7f1a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1829,7 +1829,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==7.1.0 +simplisafe-python==7.3.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0056721f04e..3f2d4736d19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -622,7 +622,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==7.1.0 +simplisafe-python==7.3.0 # homeassistant.components.sleepiq sleepyq==0.7 From 043d36f7c66a08ccee64ab2380a4578f3bfe515c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 14 Feb 2020 20:09:40 +0100 Subject: [PATCH 253/378] Change multi_select config validator to class (#31828) * Move multi_select to class * Fix serializer and add test * Serializer should also return options --- homeassistant/helpers/config_validation.py | 18 ++++++++++-------- tests/helpers/test_config_validation.py | 8 ++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0fc9dbbaae1..1ff2644fa58 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -588,22 +588,24 @@ def ensure_list_csv(value: Any) -> List: return ensure_list(value) -def multi_select(options: dict) -> Callable[[List], List]: +class multi_select: """Multi select validator returning list of selected values.""" - def validator(selected: List) -> list: - """Return list of selected values.""" + def __init__(self, options: dict) -> None: + """Initialize multi select.""" + self.options = options + + def __call__(self, selected: list) -> list: + """Validate input.""" if not isinstance(selected, list): raise vol.Invalid("Not a list") for value in selected: - if value not in options: + if value not in self.options: raise vol.Invalid(f"{value} is not a valid option") return selected - return validator - def deprecated( key: str, @@ -730,8 +732,8 @@ def custom_serializer(schema: Any) -> Any: if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} - if schema is multi_select: - return {"type": "multi_select"} + if isinstance(schema, multi_select): + return {"type": "multi_select", "options": schema.options} return voluptuous_serialize.UNSUPPORTED diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index afb215822c4..9b6aa6b812d 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -488,6 +488,14 @@ def test_multi_select(): schema(["robban", "paulus"]) +def test_multi_select_in_serializer(): + """Test multi_select with custom_serializer.""" + assert cv.custom_serializer(cv.multi_select({"paulus": "Paulus"})) == { + "type": "multi_select", + "options": {"paulus": "Paulus"}, + } + + @pytest.fixture def schema(): """Create a schema used for testing deprecation.""" From 614be5c1bb0567b9589416b2fe3002f6b4a371d2 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Fri, 14 Feb 2020 22:11:51 +0200 Subject: [PATCH 254/378] Remove energy sensor from incompatible Ata devices (#31831) An AtaDevice has a boolean flag describing whether it supports energy consumption metering. The flag was ignored resulting in sensor entities reporting constant 0 kWh consumption. * Update pymelcloud dependency to support the has_energy_consumed_meter flag. * Add ATTR_ENABLED_FN to sensor definitions for filtering out unsupported sensors. * Fix typing issue in sensor constructor. * Remove unused UnitSystem constructor parameter. --- homeassistant/components/melcloud/manifest.json | 2 +- homeassistant/components/melcloud/sensor.py | 12 ++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 43331def303..55edcdd0d9f 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.0.0"], + "requirements": ["pymelcloud==2.1.0"], "dependencies": [], "codeowners": ["@vilppuvuorinen"] } diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 428c83a4ee3..8f55906443e 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -1,12 +1,12 @@ """Support for MelCloud device sensors.""" import logging -from pymelcloud import DEVICE_TYPE_ATA, AtaDevice +from pymelcloud import DEVICE_TYPE_ATA from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.util.unit_system import UnitSystem +from . import MelCloudDevice from .const import DOMAIN, TEMP_UNIT_LOOKUP ATTR_MEASUREMENT_NAME = "measurement_name" @@ -14,6 +14,7 @@ ATTR_ICON = "icon" ATTR_UNIT_FN = "unit_fn" ATTR_DEVICE_CLASS = "device_class" ATTR_VALUE_FN = "value_fn" +ATTR_ENABLED_FN = "enabled" SENSORS = { "room_temperature": { @@ -22,6 +23,7 @@ SENSORS = { ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda x: x.device.room_temperature, + ATTR_ENABLED_FN: lambda x: True, }, "energy": { ATTR_MEASUREMENT_NAME: "Energy", @@ -29,6 +31,7 @@ SENSORS = { ATTR_UNIT_FN: lambda x: "kWh", ATTR_DEVICE_CLASS: None, ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed, + ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter, }, } @@ -40,9 +43,10 @@ async def async_setup_entry(hass, entry, async_add_entities): mel_devices = hass.data[DOMAIN].get(entry.entry_id) async_add_entities( [ - MelCloudSensor(mel_device, measurement, definition, hass.config.units) + MelCloudSensor(mel_device, measurement, definition) for measurement, definition in SENSORS.items() for mel_device in mel_devices[DEVICE_TYPE_ATA] + if definition[ATTR_ENABLED_FN](mel_device) ], True, ) @@ -51,7 +55,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class MelCloudSensor(Entity): """Representation of a Sensor.""" - def __init__(self, device: AtaDevice, measurement, definition, units: UnitSystem): + def __init__(self, device: MelCloudDevice, measurement, definition): """Initialize the sensor.""" self._api = device self._name_slug = device.name diff --git a/requirements_all.txt b/requirements_all.txt index cb9d5f7f1a0..a05b40af502 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1361,7 +1361,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.melcloud -pymelcloud==2.0.0 +pymelcloud==2.1.0 # homeassistant.components.somfy pymfy==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f2d4736d19..46b998f2f78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.melcloud -pymelcloud==2.0.0 +pymelcloud==2.1.0 # homeassistant.components.somfy pymfy==0.7.1 From f396f1cb1820f44a60c9145bd36a54ea439a4dac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 14 Feb 2020 22:58:39 +0100 Subject: [PATCH 255/378] Spotify integration hotfixes (#31835) * Remove services file, incorrect info * Guard currently playing for being a NoneType * Revert "Guard currently playing for being a NoneType" This reverts commit f5f56b0db03b407e058d45cd3549af1388916e06. * Guard currently playing item is None * Process review suggestions --- homeassistant/components/spotify/media_player.py | 9 ++++++--- homeassistant/components/spotify/services.yaml | 9 --------- 2 files changed, 6 insertions(+), 12 deletions(-) delete mode 100644 homeassistant/components/spotify/services.yaml diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 8bd5782f7ee..9588f428a66 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -157,7 +157,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice): @property def media_content_id(self) -> Optional[str]: """Return the media URL.""" - return self._currently_playing.get("item", {}).get("name") + item = self._currently_playing.get("item") or {} + return item.get("name") @property def media_content_type(self) -> Optional[str]: @@ -203,7 +204,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice): @property def media_title(self) -> Optional[str]: """Return the media title.""" - return self._currently_playing.get("item", {}).get("name") + item = self._currently_playing.get("item") or {} + return item.get("name") @property def media_artist(self) -> Optional[str]: @@ -224,7 +226,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice): @property def media_track(self) -> Optional[int]: """Track number of current playing media, music track only.""" - return self._currently_playing.get("item", {}).get("track_number") + item = self._currently_playing.get("item") or {} + return item.get("track_number") @property def media_playlist(self): diff --git a/homeassistant/components/spotify/services.yaml b/homeassistant/components/spotify/services.yaml deleted file mode 100644 index e532f736652..00000000000 --- a/homeassistant/components/spotify/services.yaml +++ /dev/null @@ -1,9 +0,0 @@ -play_playlist: - description: Play a Spotify playlist. - fields: - media_content_id: - description: Spotify URI of the playlist. - example: 'spotify:playlist:0IpRnqCHSjun48oQRX1Dy7' - random_song: - description: True to select random song at start, False to start from beginning. - example: true \ No newline at end of file From 52b045ed30cbb0c7968bedf09d9753e629b63d2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Feb 2020 15:27:31 -0800 Subject: [PATCH 256/378] Fix person device_trackers null (#31829) --- homeassistant/components/person/__init__.py | 39 ++++++++++++++++----- homeassistant/helpers/collection.py | 6 +++- tests/components/person/test_init.py | 14 +++++++- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index a1620c578e3..9cd3e882c48 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -88,7 +88,11 @@ _UNDEF = object() async def async_create_person(hass, name, *, user_id=None, device_trackers=None): """Create a new person.""" await hass.data[DOMAIN][1].async_create_item( - {ATTR_NAME: name, ATTR_USER_ID: user_id, "device_trackers": device_trackers} + { + ATTR_NAME: name, + ATTR_USER_ID: user_id, + CONF_DEVICE_TRACKERS: device_trackers or [], + } ) @@ -103,14 +107,14 @@ async def async_add_user_device_tracker( if person.get(ATTR_USER_ID) != user_id: continue - device_trackers = person["device_trackers"] + device_trackers = person[CONF_DEVICE_TRACKERS] if device_tracker_entity_id in device_trackers: return await coll.async_update_item( person[collection.CONF_ID], - {"device_trackers": device_trackers + [device_tracker_entity_id]}, + {CONF_DEVICE_TRACKERS: device_trackers + [device_tracker_entity_id]}, ) break @@ -161,6 +165,23 @@ class PersonStorageCollection(collection.StorageCollection): super().__init__(store, logger, id_manager) self.yaml_collection = yaml_collection + async def _async_load_data(self) -> Optional[dict]: + """Load the data. + + A past bug caused onboarding to create invalid person objects. + This patches it up. + """ + data = await super()._async_load_data() + + if data is None: + return data + + for person in data["items"]: + if person[CONF_DEVICE_TRACKERS] is None: + person[CONF_DEVICE_TRACKERS] = [] + + return data + async def async_load(self) -> None: """Load the Storage collection.""" await super().async_load() @@ -179,14 +200,16 @@ class PersonStorageCollection(collection.StorageCollection): return for person in list(self.data.values()): - if entity_id not in person["device_trackers"]: + if entity_id not in person[CONF_DEVICE_TRACKERS]: continue await self.async_update_item( person[collection.CONF_ID], { - "device_trackers": [ - devt for devt in person["device_trackers"] if devt != entity_id + CONF_DEVICE_TRACKERS: [ + devt + for devt in person[CONF_DEVICE_TRACKERS] + if devt != entity_id ] }, ) @@ -408,7 +431,7 @@ class Person(RestoreEntity): self._unsub_track_device() self._unsub_track_device = None - trackers = self._config.get(CONF_DEVICE_TRACKERS) + trackers = self._config[CONF_DEVICE_TRACKERS] if trackers: _LOGGER.debug("Subscribe to device trackers for %s", self.entity_id) @@ -428,7 +451,7 @@ class Person(RestoreEntity): def _update_state(self): """Update the state.""" latest_non_gps_home = latest_not_home = latest_gps = latest = None - for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []): + for entity_id in self._config[CONF_DEVICE_TRACKERS]: state = self.hass.states.get(entity_id) if not state or state.state in IGNORE_STATES: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 1b3721788f5..025c6c07dee 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -158,9 +158,13 @@ class StorageCollection(ObservableCollection): """Home Assistant object.""" return self.store.hass + async def _async_load_data(self) -> Optional[dict]: + """Load the data.""" + return cast(Optional[dict], await self.store.async_load()) + async def async_load(self) -> None: """Load the storage Manager.""" - raw_storage = cast(Optional[dict], await self.store.async_load()) + raw_storage = await self._async_load_data() if raw_storage is None: raw_storage = {"items": []} diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index e5a414d95ad..76350619983 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,7 +1,7 @@ """The tests for the person component.""" import logging -from unittest.mock import patch +from asynctest import patch import pytest from homeassistant.components import person @@ -773,3 +773,15 @@ async def test_reload(hass, hass_admin_user): assert state_2 is None assert state_3 is not None assert state_3.name == "Person 3" + + +async def test_person_storage_fixing_device_trackers(storage_collection): + """Test None device trackers become lists.""" + with patch.object( + storage_collection.store, + "async_load", + return_value={"items": [{"id": "bla", "name": "bla", "device_trackers": None}]}, + ): + await storage_collection.async_load() + + assert storage_collection.data["bla"]["device_trackers"] == [] From 4501471b8ce460cc46deafaafec5f3ab49006df7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Feb 2020 15:28:11 -0800 Subject: [PATCH 257/378] Google Assistant: Remove speaker type and earlier filter out devices from being locally exposed (#31830) * Remove speaker type * Do not expose locks or alarms to Google Local --- .../components/google_assistant/const.py | 3 ++- .../components/google_assistant/helpers.py | 15 ++++++++++++++- .../components/google_assistant/smart_home.py | 4 +--- tests/components/google_assistant/test_helpers.py | 10 ++++++++++ .../google_assistant/test_smart_home.py | 1 - 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index add625d2de4..c9f8d857b62 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -133,7 +133,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, - (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, } @@ -146,3 +145,5 @@ STORE_AGENT_USER_IDS = "agent_user_ids" SOURCE_CLOUD = "cloud" SOURCE_LOCAL = "local" + +NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK} diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index f1b7a89bffe..6ba301c01e8 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -28,6 +28,7 @@ from .const import ( DOMAIN, DOMAIN_TO_GOOGLE_TYPES, ERR_FUNCTION_NOT_SUPPORTED, + NOT_EXPOSE_LOCAL, SOURCE_LOCAL, STORE_AGENT_USER_IDS, ) @@ -351,6 +352,18 @@ class GoogleEntity: """If entity should be exposed.""" return self.config.should_expose(self.state) + @callback + def should_expose_local(self) -> bool: + """Return if the entity should be exposed locally.""" + return ( + self.should_expose() + and get_google_type( + self.state.domain, self.state.attributes.get(ATTR_DEVICE_CLASS) + ) + not in NOT_EXPOSE_LOCAL + and not self.might_2fa() + ) + @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" @@ -401,7 +414,7 @@ class GoogleEntity: if aliases: device["name"]["nicknames"] = [name] + aliases - if self.config.is_local_sdk_active: + if self.config.is_local_sdk_active and self.should_expose_local(): device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.local_sdk_webhook_id, diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index bf6c32505aa..97c872bdaf8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -243,9 +243,7 @@ async def async_devices_reachable(hass, data: RequestData, payload): "devices": [ entity.reachable_device_serialize() for entity in async_get_entities(hass, data.config) - if entity.entity_id in google_ids - and entity.should_expose() - and not entity.might_2fa() + if entity.entity_id in google_ids and entity.should_expose_local() ] } diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 9c8a868e68d..8d2aaa63c48 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.google_assistant import helpers from homeassistant.components.google_assistant.const import ( # noqa: F401 EVENT_COMMAND_RECEIVED, + NOT_EXPOSE_LOCAL, ) from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -46,6 +47,15 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): "webhookId": "mock-webhook-id", } + for device_type in NOT_EXPOSE_LOCAL: + with patch( + "homeassistant.components.google_assistant.helpers.get_google_type", + return_value=device_type, + ): + serialized = await entity.sync_serialize(None) + assert "otherDeviceIds" not in serialized + assert "customData" not in serialized + async def test_config_local_sdk(hass, hass_client): """Test the local SDK.""" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index aa073c699f8..7e98f162f22 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -682,7 +682,6 @@ async def test_device_class_cover(hass, device_class, google_type): "device_class,google_type", [ ("non_existing_class", "action.devices.types.SWITCH"), - ("speaker", "action.devices.types.SPEAKER"), ("tv", "action.devices.types.TV"), ], ) From f3a8196fb5815fc2abbcc6f4f2f4ab0e0758f067 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 15 Feb 2020 00:31:45 +0000 Subject: [PATCH 258/378] [ci skip] Translation update --- homeassistant/components/axis/.translations/zh-Hant.json | 2 +- homeassistant/components/gdacs/.translations/pl.json | 9 +++++++-- .../homekit_controller/.translations/zh-Hant.json | 2 +- homeassistant/components/ipma/.translations/en.json | 1 + homeassistant/components/ipma/.translations/ru.json | 1 + homeassistant/components/konnected/.translations/pl.json | 6 +++--- homeassistant/components/locative/.translations/pl.json | 2 +- homeassistant/components/melcloud/.translations/pl.json | 8 ++++++-- homeassistant/components/mikrotik/.translations/pl.json | 2 +- .../components/minecraft_server/.translations/pl.json | 3 +++ homeassistant/components/traccar/.translations/pl.json | 2 +- homeassistant/components/vilfo/.translations/ca.json | 4 ++++ homeassistant/components/vilfo/.translations/pl.json | 7 +++++++ 13 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/vilfo/.translations/pl.json diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index 751a7544202..ac552afe583 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548", + "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548\u932f\u8aa4", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099", "updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a" diff --git a/homeassistant/components/gdacs/.translations/pl.json b/homeassistant/components/gdacs/.translations/pl.json index 3932e3398bc..f4b90cc35c7 100644 --- a/homeassistant/components/gdacs/.translations/pl.json +++ b/homeassistant/components/gdacs/.translations/pl.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana." + }, "step": { "user": { "data": { "radius": "Promie\u0144" - } + }, + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." } - } + }, + "title": "Globalny system ostrzegania i koordynacji w przypadku katastrof (GDACS)" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json index 68e87e9aea8..c2092c2016b 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002", - "invalid_config_entry": "\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "invalid_config_entry": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { diff --git a/homeassistant/components/ipma/.translations/en.json b/homeassistant/components/ipma/.translations/en.json index 15459b91f2a..d47f0dfb501 100644 --- a/homeassistant/components/ipma/.translations/en.json +++ b/homeassistant/components/ipma/.translations/en.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitude", "longitude": "Longitude", + "mode": "Mode", "name": "Name" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index 0db504c629c..96d4ee90407 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -8,6 +8,7 @@ "data": { "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b.", diff --git a/homeassistant/components/konnected/.translations/pl.json b/homeassistant/components/konnected/.translations/pl.json index 398a75ae58e..d3e6240af49 100644 --- a/homeassistant/components/konnected/.translations/pl.json +++ b/homeassistant/components/konnected/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d." }, "title": "Konnected.io" }, @@ -16,7 +16,7 @@ "options_digital": { "data": { "name": "Nazwa (opcjonalnie)", - "type": "Typ czujnika" + "type": "Typ sensora" } }, "options_switch": { diff --git a/homeassistant/components/locative/.translations/pl.json b/homeassistant/components/locative/.translations/pl.json index 9c22a8e3fea..23a4c98a54c 100644 --- a/homeassistant/components/locative/.translations/pl.json +++ b/homeassistant/components/locative/.translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Na pewno chcesz skonfigurowa\u0107 Locative Webhook?", - "title": "Skonfiguruj Locative Webhook" + "title": "Konfiguracja Locative Webhook" } }, "title": "Locative Webhook" diff --git a/homeassistant/components/melcloud/.translations/pl.json b/homeassistant/components/melcloud/.translations/pl.json index c374e7d3e2a..e996cf92d2e 100644 --- a/homeassistant/components/melcloud/.translations/pl.json +++ b/homeassistant/components/melcloud/.translations/pl.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "Integracja MELCloud jest ju\u017c skonfigurowana dla tego adresu e-mail. Token dost\u0119pu zosta\u0142 od\u015bwie\u017cony." + }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Niespodziewany b\u0142\u0105d" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." }, "title": "MELCloud" } diff --git a/homeassistant/components/mikrotik/.translations/pl.json b/homeassistant/components/mikrotik/.translations/pl.json index 1ca6fa7d1bb..6d807672398 100644 --- a/homeassistant/components/mikrotik/.translations/pl.json +++ b/homeassistant/components/mikrotik/.translations/pl.json @@ -18,7 +18,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "U\u017cyj SSL" }, - "title": "Skonfiguruj router Mikrotik" + "title": "Konfiguracja routera Mikrotik" } }, "title": "Mikrotik" diff --git a/homeassistant/components/minecraft_server/.translations/pl.json b/homeassistant/components/minecraft_server/.translations/pl.json index 94f9fd20af3..f9c4a515566 100644 --- a/homeassistant/components/minecraft_server/.translations/pl.json +++ b/homeassistant/components/minecraft_server/.translations/pl.json @@ -4,6 +4,8 @@ "already_configured": "Host jest ju\u017c skonfigurowany." }, "error": { + "cannot_connect": "B\u0142\u0105d po\u0142\u0105czenia z serwerem. Sprawd\u017a adres hosta i port i spr\u00f3buj ponownie. Upewnij si\u0119 tak\u017ce, \u017ce na serwerze dzia\u0142a Minecraft w wersji przynajmniej 1.7.", + "invalid_ip": "Adres IP jest nieprawid\u0142owy (nie mo\u017cna ustali\u0107 adresu MAC). Popraw to i spr\u00f3buj ponownie.", "invalid_port": "Port musi znajdowa\u0107 si\u0119 w zakresie od 1024 do 65535. Popraw go i spr\u00f3buj ponownie." }, "step": { @@ -13,6 +15,7 @@ "name": "Nazwa", "port": "Port" }, + "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.", "title": "Po\u0142\u0105cz sw\u00f3j serwer Minecraft" } }, diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json index 95b7eb1af00..b7eaf7fe16e 100644 --- a/homeassistant/components/traccar/.translations/pl.json +++ b/homeassistant/components/traccar/.translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Na pewno chcesz skonfigurowa\u0107 Traccar?", - "title": "Skonfiguruj Traccar" + "title": "Konfiguracja Traccar" } }, "title": "Traccar" diff --git a/homeassistant/components/vilfo/.translations/ca.json b/homeassistant/components/vilfo/.translations/ca.json index e788da3b552..fbb46526f2b 100644 --- a/homeassistant/components/vilfo/.translations/ca.json +++ b/homeassistant/components/vilfo/.translations/ca.json @@ -3,9 +3,13 @@ "abort": { "already_configured": "L'encaminador Vilfo ja est\u00e0 configurat." }, + "error": { + "unknown": "S'ha produ\u00eft un error inesperat durant la configuraci\u00f3 de la integraci\u00f3." + }, "step": { "user": { "data": { + "access_token": "Testimoni d'acc\u00e9s per l'API de l'encaminador Vilfo", "host": "Nom d'amfitri\u00f3 o IP de l'encaminador" }, "title": "Connexi\u00f3 amb l'encaminador Vilfo" diff --git a/homeassistant/components/vilfo/.translations/pl.json b/homeassistant/components/vilfo/.translations/pl.json new file mode 100644 index 00000000000..9af4d104965 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ten router Vilfo jest ju\u017c skonfigurowany." + } + } +} \ No newline at end of file From 4e54dfa8749a20836c6cdab161df7f6bc7d4353c Mon Sep 17 00:00:00 2001 From: Massimiliano Cannarozzo Date: Sat, 15 Feb 2020 10:57:04 +0100 Subject: [PATCH 259/378] Add turn_on_action configuration variable (#31792) Allow to turn on the TV using an external service --- .../components/lg_netcast/media_player.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 0be51c337e8..49e231cc6d3 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -16,6 +16,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, ) @@ -28,11 +29,14 @@ from homeassistant.const import ( STATE_PLAYING, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "LG TV Remote" +CONF_ON_ACTION = "turn_on_action" + MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -49,6 +53,7 @@ SUPPORT_LGTV = ( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -62,20 +67,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME) + on_action = config.get(CONF_ON_ACTION) client = LgNetCastClient(host, access_token) + on_action_script = Script(hass, on_action) if on_action else None - add_entities([LgTVDevice(client, name)], True) + add_entities([LgTVDevice(client, name, on_action_script)], True) class LgTVDevice(MediaPlayerDevice): """Representation of a LG TV.""" - def __init__(self, client, name): + def __init__(self, client, name, on_action_script): """Initialize the LG TV device.""" self._client = client self._name = name self._muted = False + self._on_action_script = on_action_script # Assume that the TV is in Play mode self._playing = True self._volume = 0 @@ -180,6 +188,8 @@ class LgTVDevice(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" + if self._on_action_script: + return SUPPORT_LGTV | SUPPORT_TURN_ON return SUPPORT_LGTV @property @@ -191,6 +201,11 @@ class LgTVDevice(MediaPlayerDevice): """Turn off media player.""" self.send_command(1) + def turn_on(self): + """Turn on the media player.""" + if self._on_action_script: + self._on_action_script.run() + def volume_up(self): """Volume up the media player.""" self.send_command(24) From e38522c612a0d912e931db6924ca15314c2dbdc6 Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 15 Feb 2020 10:59:41 +0000 Subject: [PATCH 260/378] Bump pillow to 7.0 (#31847) --- homeassistant/components/doods/manifest.json | 7 +++++-- homeassistant/components/proxy/manifest.json | 6 ++++-- homeassistant/components/qrcode/manifest.json | 7 +++++-- homeassistant/components/seven_segments/manifest.json | 6 ++++-- homeassistant/components/tensorflow/manifest.json | 4 ++-- requirements_all.txt | 2 +- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 551af839b5c..1ac905feac2 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,10 @@ "domain": "doods", "name": "DOODS - Distributed Outside Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==6.2.1"], + "requirements": [ + "pydoods==1.0.2", + "pillow==7.0.0" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 06498e51ad3..d12fbe2d3d7 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,7 +2,9 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==6.2.1"], + "requirements": [ + "pillow==7.0.0" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 6ea6db621fb..cc2cde26aa5 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,10 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==6.2.1", "pyzbar==0.1.7"], + "requirements": [ + "pillow==7.0.0", + "pyzbar==0.1.7" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 672679b7254..eba33e75f71 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,9 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==6.2.1"], + "requirements": [ + "pillow==7.0.0" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 03a5db01a47..024dc2b7bdd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,8 +6,8 @@ "tensorflow==1.13.2", "numpy==1.18.1", "protobuf==3.6.1", - "pillow==6.2.1" + "pillow==7.0.0" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index a05b40af502..116ff23f1ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ pilight==0.1.1 # homeassistant.components.qrcode # homeassistant.components.seven_segments # homeassistant.components.tensorflow -pillow==6.2.1 +pillow==7.0.0 # homeassistant.components.dominos pizzapi==0.0.3 From 1609e33030bab3c9f14954af4cf94b869491b619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 15 Feb 2020 17:53:10 +0200 Subject: [PATCH 261/378] Simplify missing Garmin Connect data handling, mark entities un/available (#31718) * Simplify missing Garmin Connect data handling, mark entities un/available * Remove unnecessary else --- .../components/garmin_connect/sensor.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index d3f95b162bf..5edf54d95dc 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -160,21 +160,20 @@ class GarminConnectSensor(Entity): return await self._data.async_update() - if not self._data.data: + data = self._data.data + if not data: _LOGGER.error("Didn't receive data from Garmin Connect") return - - data = self._data.data - try: - if "Duration" in self._type and data[self._type]: - self._state = data[self._type] // 60 - elif "Seconds" in self._type and data[self._type]: - self._state = data[self._type] // 60 - else: - self._state = data[self._type] - except KeyError: - _LOGGER.debug("Entity type %s not found in fetched data", self._type) + if data.get(self._type) is None: + _LOGGER.debug("Entity type %s not set in fetched data", self._type) + self._available = False return + self._available = True + + if "Duration" in self._type or "Seconds" in self._type: + self._state = data[self._type] // 60 + else: + self._state = data[self._type] _LOGGER.debug( "Entity %s set to state %s %s", self._type, self._state, self._unit From b8f9ff76b3e08c64273c884b4fa9b5cd58b18f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sat, 15 Feb 2020 17:08:21 +0100 Subject: [PATCH 262/378] Add Tado water_heater (#30095) * Add Tado water_heater * Don't use climate CONSTS * Fix logging text * Add changes for multiple bridge support * Address remarks * should_poll must be False * Remove additional async_schedule_update_ha_state() * Not for climate --- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tado/climate.py | 67 ++-- homeassistant/components/tado/manifest.json | 8 +- homeassistant/components/tado/water_heater.py | 302 ++++++++++++++++++ 4 files changed, 343 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/tado/water_heater.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index dbc4e87b650..727fb868a33 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -20,7 +20,7 @@ DOMAIN = "tado" SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}" -TADO_COMPONENTS = ["sensor", "climate"] +TADO_COMPONENTS = ["sensor", "climate", "water_heater"] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 44e35bce787..b92a54edd5e 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -25,13 +25,16 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, + DATA, TYPE_AIR_CONDITIONING, + TYPE_HEATING, ) _LOGGER = logging.getLogger(__name__) @@ -39,25 +42,25 @@ _LOGGER = logging.getLogger(__name__) FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} HVAC_MAP_TADO_HEAT = { - "MANUAL": HVAC_MODE_HEAT, - "TIMER": HVAC_MODE_HEAT, - "TADO_MODE": HVAC_MODE_HEAT, - "SMART_SCHEDULE": HVAC_MODE_AUTO, - "OFF": HVAC_MODE_OFF, + CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT, + CONST_OVERLAY_TIMER: HVAC_MODE_HEAT, + CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT, + CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, + CONST_MODE_OFF: HVAC_MODE_OFF, } HVAC_MAP_TADO_COOL = { - "MANUAL": HVAC_MODE_COOL, - "TIMER": HVAC_MODE_COOL, - "TADO_MODE": HVAC_MODE_COOL, - "SMART_SCHEDULE": HVAC_MODE_AUTO, - "OFF": HVAC_MODE_OFF, + CONST_OVERLAY_MANUAL: HVAC_MODE_COOL, + CONST_OVERLAY_TIMER: HVAC_MODE_COOL, + CONST_OVERLAY_TADO_MODE: HVAC_MODE_COOL, + CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, + CONST_MODE_OFF: HVAC_MODE_OFF, } HVAC_MAP_TADO_HEAT_COOL = { - "MANUAL": HVAC_MODE_HEAT_COOL, - "TIMER": HVAC_MODE_HEAT_COOL, - "TADO_MODE": HVAC_MODE_HEAT_COOL, - "SMART_SCHEDULE": HVAC_MODE_AUTO, - "OFF": HVAC_MODE_OFF, + CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT_COOL, + CONST_OVERLAY_TIMER: HVAC_MODE_HEAT_COOL, + CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT_COOL, + CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, + CONST_MODE_OFF: HVAC_MODE_OFF, } SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -70,14 +73,18 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" + if discovery_info is None: + return + api_list = hass.data[DOMAIN][DATA] entities = [] for tado in api_list: for zone in tado.zones: - entity = create_climate_entity(tado, zone["name"], zone["id"]) - if entity: - entities.append(entity) + if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]: + entity = create_climate_entity(tado, zone["name"], zone["id"]) + if entity: + entities.append(entity) if entities: add_entities(entities, True) @@ -154,11 +161,9 @@ class TadoClimate(ClimateDevice): self._target_temp = None if tado.fallback: - _LOGGER.debug("Default overlay is set to TADO MODE") # Fallback to Smart Schedule at next Schedule switch self._default_overlay = CONST_OVERLAY_TADO_MODE else: - _LOGGER.debug("Default overlay is set to MANUAL MODE") # Don't fallback to Smart Schedule, but keep in manual mode self._default_overlay = CONST_OVERLAY_MANUAL @@ -354,11 +359,7 @@ class TadoClimate(ClimateDevice): def update(self): """Handle update callbacks.""" _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) - try: - data = self._tado.data["zone"][self.zone_id] - except KeyError: - _LOGGER.debug("No data") - return + data = self._tado.data["zone"][self.zone_id] if "sensorDataPoints" in data: sensor_data = data["sensorDataPoints"] @@ -371,13 +372,13 @@ class TadoClimate(ClimateDevice): humidity = float(sensor_data["humidity"]["percentage"]) self._cur_humidity = humidity - # temperature setting will not exist when device is off - if ( - "temperature" in data["setting"] - and data["setting"]["temperature"] is not None - ): - setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = setting + # temperature setting will not exist when device is off + if ( + "temperature" in data["setting"] + and data["setting"]["temperature"] is not None + ): + setting = float(data["setting"]["temperature"]["celsius"]) + self._target_temp = setting if "tadoMode" in data: mode = data["tadoMode"] diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 7539988d42e..4728f1622ed 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -2,7 +2,11 @@ "domain": "tado", "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", - "requirements": ["python-tado==0.2.9"], + "requirements": [ + "python-tado==0.2.9" + ], "dependencies": [], - "codeowners": ["@michaelarnauts"] + "codeowners": [ + "@michaelarnauts" + ] } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py new file mode 100644 index 00000000000..fc3a9ce9cf4 --- /dev/null +++ b/homeassistant/components/tado/water_heater.py @@ -0,0 +1,302 @@ +"""Support for Tado hot water zones.""" +import logging + +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import ( + CONST_MODE_OFF, + CONST_MODE_SMART_SCHEDULE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, + DATA, + TYPE_HOT_WATER, +) + +_LOGGER = logging.getLogger(__name__) + +MODE_AUTO = "auto" +MODE_HEAT = "heat" +MODE_OFF = "off" + +OPERATION_MODES = [MODE_AUTO, MODE_HEAT, MODE_OFF] + +WATER_HEATER_MAP_TADO = { + CONST_OVERLAY_MANUAL: MODE_HEAT, + CONST_OVERLAY_TIMER: MODE_HEAT, + CONST_OVERLAY_TADO_MODE: MODE_HEAT, + CONST_MODE_SMART_SCHEDULE: MODE_AUTO, + CONST_MODE_OFF: MODE_OFF, +} + +SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Tado water heater platform.""" + if discovery_info is None: + return + + api_list = hass.data[DOMAIN][DATA] + entities = [] + + for tado in api_list: + for zone in tado.zones: + if zone["type"] in [TYPE_HOT_WATER]: + entity = create_water_heater_entity(tado, zone["name"], zone["id"]) + entities.append(entity) + + if entities: + add_entities(entities, True) + + +def create_water_heater_entity(tado, name: str, zone_id: int): + """Create a Tado water heater device.""" + capabilities = tado.get_capabilities(zone_id) + supports_temperature_control = capabilities["canSetTemperature"] + + if supports_temperature_control and "temperatures" in capabilities: + temperatures = capabilities["temperatures"] + min_temp = float(temperatures["celsius"]["min"]) + max_temp = float(temperatures["celsius"]["max"]) + else: + min_temp = None + max_temp = None + + entity = TadoWaterHeater( + tado, name, zone_id, supports_temperature_control, min_temp, max_temp + ) + + return entity + + +class TadoWaterHeater(WaterHeaterDevice): + """Representation of a Tado water heater.""" + + def __init__( + self, + tado, + zone_name, + zone_id, + supports_temperature_control, + min_temp, + max_temp, + ): + """Initialize of Tado water heater entity.""" + self._tado = tado + + self.zone_name = zone_name + self.zone_id = zone_id + self._unique_id = f"{zone_id} {tado.device_id}" + + self._device_is_active = False + self._is_away = False + + self._supports_temperature_control = supports_temperature_control + self._min_temperature = min_temp + self._max_temperature = max_temp + + self._target_temp = None + + self._supported_features = SUPPORT_FLAGS_HEATER + if self._supports_temperature_control: + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + if tado.fallback: + # Fallback to Smart Schedule at next Schedule switch + self._default_overlay = CONST_OVERLAY_TADO_MODE + else: + # Don't fallback to Smart Schedule, but keep in manual mode + self._default_overlay = CONST_OVERLAY_MANUAL + + self._current_operation = CONST_MODE_SMART_SCHEDULE + self._overlay_mode = CONST_MODE_SMART_SCHEDULE + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + async_update_callback, + ) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._supported_features + + @property + def name(self): + """Return the name of the entity.""" + return self.zone_name + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + + @property + def current_operation(self): + """Return current readable operation mode.""" + return WATER_HEATER_MAP_TADO.get(self._current_operation) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temp + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._is_away + + @property + def operation_list(self): + """Return the list of available operation modes (readable).""" + return OPERATION_MODES + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._min_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._max_temperature + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + mode = None + + if operation_mode == MODE_OFF: + mode = CONST_MODE_OFF + elif operation_mode == MODE_AUTO: + mode = CONST_MODE_SMART_SCHEDULE + elif operation_mode == MODE_HEAT: + mode = self._default_overlay + + self._current_operation = mode + self._overlay_mode = None + + # Set a target temperature if we don't have any + if mode == CONST_OVERLAY_TADO_MODE and self._target_temp is None: + self._target_temp = self.min_temp + + self._control_heater() + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if not self._supports_temperature_control or temperature is None: + return + + self._current_operation = self._default_overlay + self._overlay_mode = None + self._target_temp = temperature + self._control_heater() + + def update(self): + """Handle update callbacks.""" + _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id) + data = self._tado.data["zone"][self.zone_id] + + if "tadoMode" in data: + mode = data["tadoMode"] + self._is_away = mode == "AWAY" + + if "setting" in data: + power = data["setting"]["power"] + if power == "OFF": + self._current_operation = CONST_MODE_OFF + # There is no overlay, the mode will always be + # "SMART_SCHEDULE" + self._overlay_mode = CONST_MODE_SMART_SCHEDULE + self._device_is_active = False + else: + self._device_is_active = True + + # temperature setting will not exist when device is off + if ( + "temperature" in data["setting"] + and data["setting"]["temperature"] is not None + ): + setting = float(data["setting"]["temperature"]["celsius"]) + self._target_temp = setting + + overlay = False + overlay_data = None + termination = CONST_MODE_SMART_SCHEDULE + + if "overlay" in data: + overlay_data = data["overlay"] + overlay = overlay_data is not None + + if overlay: + termination = overlay_data["termination"]["type"] + + if self._device_is_active: + # If you set mode manually to off, there will be an overlay + # and a termination, but we want to see the mode "OFF" + self._overlay_mode = termination + self._current_operation = termination + + def _control_heater(self): + """Send new target temperature.""" + if self._current_operation == CONST_MODE_SMART_SCHEDULE: + _LOGGER.debug( + "Switching to SMART_SCHEDULE for zone %s (%d)", + self.zone_name, + self.zone_id, + ) + self._tado.reset_zone_overlay(self.zone_id) + self._overlay_mode = self._current_operation + return + + if self._current_operation == CONST_MODE_OFF: + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) + self._overlay_mode = self._current_operation + return + + _LOGGER.debug( + "Switching to %s for zone %s (%d) with temperature %s", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._tado.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + TYPE_HOT_WATER, + ) + self._overlay_mode = self._current_operation From 588f2cd920f955578f97edcab180e1f08d3a14c0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 15 Feb 2020 17:21:04 +0100 Subject: [PATCH 263/378] Revert "Check netgear device_tracker link_rate to ensure device is connected" (#31855) This reverts commit 669844e4ddc7c3733ef03e4baf0e3ba6d1c5c3a6. --- homeassistant/components/netgear/device_tracker.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 23b1034a5b3..3e87bcac53c 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -110,10 +110,7 @@ class NetgearDeviceScanner(DeviceScanner): or dev.name in self.excluded_devices ) ) - - # when link_rate is None this means the router still knows about - # the device, but it is not in range. - if tracked and dev.link_rate is not None: + if tracked: devices.append(dev.mac) if ( self.tracked_accesspoints From 733f1e1101b2f51adaef7cee557d4cd12171885a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 15 Feb 2020 23:03:53 +0200 Subject: [PATCH 264/378] Helpers typing improvements (#31865) --- homeassistant/helpers/restore_state.py | 18 +++++++++--------- homeassistant/helpers/template.py | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 7b2ec8c7f75..d57d3ad9920 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Dict, List, Optional, Set +from typing import Any, Awaitable, Dict, List, Optional, Set, cast from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( @@ -20,9 +20,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs -# mypy: no-warn-return-any - DATA_RESTORE_STATE_TASK = "restore_state_task" _LOGGER = logging.getLogger(__name__) @@ -45,7 +42,7 @@ class StoredState: self.state = state self.last_seen = last_seen - def as_dict(self) -> Dict: + def as_dict(self) -> Dict[str, Any]: """Return a dict representation of the stored state.""" return {"state": self.state.as_dict(), "last_seen": self.last_seen} @@ -104,7 +101,7 @@ class RestoreStateData: load_instance(hass) ) - return await task + return await cast(Awaitable["RestoreStateData"], task) def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" @@ -211,15 +208,18 @@ class RestoreStateData: self.entity_ids.remove(entity_id) -def _encode(value): +def _encode(value: Any) -> Any: """Little helper to JSON encode a value.""" try: - return JSONEncoder.default(None, value) + return JSONEncoder.default( + None, # type: ignore + value, + ) except TypeError: return value -def _encode_complex(value): +def _encode_complex(value: Any) -> Any: """Recursively encode all values with the JSONEncoder.""" if isinstance(value, dict): return {_encode(key): _encode_complex(value) for key, value in value.items()} diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 77642ce5052..e7f89b482e2 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -378,7 +378,7 @@ class DomainStates: raise TemplateError(f"Invalid entity ID '{entity_id}'") return _get_state(self._hass, entity_id) - def _collect_domain(self): + def _collect_domain(self) -> None: entity_collect = self._hass.data.get(_RENDER_INFO) if entity_collect is not None: # pylint: disable=protected-access @@ -398,12 +398,12 @@ class DomainStates: ) ) - def __len__(self): + def __len__(self) -> int: """Return number of states.""" self._collect_domain() return len(self._hass.states.async_entity_ids(self._domain)) - def __repr__(self): + def __repr__(self) -> str: """Representation of Domain States.""" return f"