From 7cd17dd94f5fb091cd4460aca0d71a0b9ad0b615 Mon Sep 17 00:00:00 2001 From: Elena Rogleva Date: Wed, 2 Dec 2020 23:15:53 +0200 Subject: [PATCH 001/302] Rewrite the kira/test_init.py unittests to pytest style test functions (#42753) --- tests/components/kira/test_init.py | 101 +++++++++++++++-------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index b57d8c9761..db8eb6b245 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -3,13 +3,13 @@ import os import shutil import tempfile -import unittest + +import pytest import homeassistant.components.kira as kira -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.async_mock import MagicMock, patch -from tests.common import get_test_home_assistant TEST_CONFIG = { kira.DOMAIN: { @@ -31,57 +31,58 @@ KIRA_CODES = """ """ -class TestKiraSetup(unittest.TestCase): - """Test class for kira.""" +@pytest.fixture(autouse=True) +def setup_comp(): + """Set up things to be run when tests are started.""" + _base_mock = MagicMock() + pykira = _base_mock.pykira + pykira.__file__ = "test" + _module_patcher = patch.dict("sys.modules", {"pykira": pykira}) + _module_patcher.start() + yield + _module_patcher.stop() - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - _base_mock = MagicMock() - pykira = _base_mock.pykira - pykira.__file__ = "test" - self._module_patcher = patch.dict("sys.modules", {"pykira": pykira}) - self._module_patcher.start() - self.work_dir = tempfile.mkdtemp() - self.addCleanup(self.tear_down_cleanup) +@pytest.fixture(scope="module") +def work_dir(): + """Set up temporary workdir.""" + work_dir = tempfile.mkdtemp() + yield work_dir + shutil.rmtree(work_dir, ignore_errors=True) - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - self._module_patcher.stop() - shutil.rmtree(self.work_dir, ignore_errors=True) - def test_kira_empty_config(self): - """Kira component should load a default sensor.""" - setup_component(self.hass, kira.DOMAIN, {}) - assert len(self.hass.data[kira.DOMAIN]["sensor"]) == 1 +async def test_kira_empty_config(hass): + """Kira component should load a default sensor.""" + await async_setup_component(hass, kira.DOMAIN, {kira.DOMAIN: {}}) + assert len(hass.data[kira.DOMAIN]["sensor"]) == 1 - def test_kira_setup(self): - """Ensure platforms are loaded correctly.""" - setup_component(self.hass, kira.DOMAIN, TEST_CONFIG) - assert len(self.hass.data[kira.DOMAIN]["sensor"]) == 2 - assert sorted(self.hass.data[kira.DOMAIN]["sensor"].keys()) == [ - "kira", - "kira_1", - ] - assert len(self.hass.data[kira.DOMAIN]["remote"]) == 2 - assert sorted(self.hass.data[kira.DOMAIN]["remote"].keys()) == [ - "kira", - "kira_1", - ] - def test_kira_creates_codes(self): - """Kira module should create codes file if missing.""" - code_path = os.path.join(self.work_dir, "codes.yaml") - kira.load_codes(code_path) - assert os.path.exists(code_path), "Kira component didn't create codes file" +async def test_kira_setup(hass): + """Ensure platforms are loaded correctly.""" + await async_setup_component(hass, kira.DOMAIN, TEST_CONFIG) + assert len(hass.data[kira.DOMAIN]["sensor"]) == 2 + assert sorted(hass.data[kira.DOMAIN]["sensor"].keys()) == [ + "kira", + "kira_1", + ] + assert len(hass.data[kira.DOMAIN]["remote"]) == 2 + assert sorted(hass.data[kira.DOMAIN]["remote"].keys()) == [ + "kira", + "kira_1", + ] - def test_load_codes(self): - """Kira should ignore invalid codes.""" - code_path = os.path.join(self.work_dir, "codes.yaml") - with open(code_path, "w") as code_file: - code_file.write(KIRA_CODES) - res = kira.load_codes(code_path) - assert len(res) == 1, "Expected exactly 1 valid Kira code" + +async def test_kira_creates_codes(work_dir): + """Kira module should create codes file if missing.""" + code_path = os.path.join(work_dir, "codes.yaml") + kira.load_codes(code_path) + assert os.path.exists(code_path), "Kira component didn't create codes file" + + +async def test_load_codes(work_dir): + """Kira should ignore invalid codes.""" + code_path = os.path.join(work_dir, "codes.yaml") + with open(code_path, "w") as code_file: + code_file.write(KIRA_CODES) + res = kira.load_codes(code_path) + assert len(res) == 1, "Expected exactly 1 valid Kira code" From 7c8309243157798880679d9059cf26ff3bef47d7 Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Wed, 2 Dec 2020 15:28:17 -0600 Subject: [PATCH 002/302] Add Kuler Sky Bluetooth floor lamp integration (#42372) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/kulersky/__init__.py | 44 +++ .../components/kulersky/config_flow.py | 29 ++ homeassistant/components/kulersky/const.py | 2 + homeassistant/components/kulersky/light.py | 210 ++++++++++++ .../components/kulersky/manifest.json | 12 + .../components/kulersky/strings.json | 13 + .../components/kulersky/translations/en.json | 13 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/kulersky/__init__.py | 1 + tests/components/kulersky/test_config_flow.py | 104 ++++++ tests/components/kulersky/test_light.py | 315 ++++++++++++++++++ 14 files changed, 751 insertions(+) create mode 100644 homeassistant/components/kulersky/__init__.py create mode 100644 homeassistant/components/kulersky/config_flow.py create mode 100644 homeassistant/components/kulersky/const.py create mode 100644 homeassistant/components/kulersky/light.py create mode 100644 homeassistant/components/kulersky/manifest.json create mode 100644 homeassistant/components/kulersky/strings.json create mode 100644 homeassistant/components/kulersky/translations/en.json create mode 100644 tests/components/kulersky/__init__.py create mode 100644 tests/components/kulersky/test_config_flow.py create mode 100644 tests/components/kulersky/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index fe3af4c1ee..c6deb8e9f8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -242,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py new file mode 100644 index 0000000000..ff984e2c0d --- /dev/null +++ b/homeassistant/components/kulersky/__init__.py @@ -0,0 +1,44 @@ +"""Kuler Sky lights integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Kuler Sky component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Kuler Sky from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py new file mode 100644 index 0000000000..2b22fcdbd3 --- /dev/null +++ b/homeassistant/components/kulersky/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Kuler Sky.""" +import logging + +import pykulersky + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + # Check if there are any devices that can be discovered in the network. + try: + devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + except pykulersky.PykulerskyException as exc: + _LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc) + return False + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py new file mode 100644 index 0000000000..ae1e7a435d --- /dev/null +++ b/homeassistant/components/kulersky/const.py @@ -0,0 +1,2 @@ +"""Constants for the Kuler Sky integration.""" +DOMAIN = "kulersky" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py new file mode 100644 index 0000000000..4c17d1bcba --- /dev/null +++ b/homeassistant/components/kulersky/light.py @@ -0,0 +1,210 @@ +"""Kuler Sky light platform.""" +import asyncio +from datetime import timedelta +import logging +from typing import Callable, List + +import pykulersky + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE + +DISCOVERY_INTERVAL = timedelta(seconds=60) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Kuler sky light devices.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if "devices" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["devices"] = set() + if "discovery" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["discovery"] = asyncio.Lock() + + async def discover(*args): + """Attempt to discover new lights.""" + # Since discovery needs to connect to all discovered bluetooth devices, and + # only rules out devices after a timeout, it can potentially take a long + # time. If there's already a discovery running, just skip this poll. + if hass.data[DOMAIN]["discovery"].locked(): + return + + async with hass.data[DOMAIN]["discovery"]: + bluetooth_devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + + # Filter out already connected lights + new_devices = [ + device + for device in bluetooth_devices + if device["address"] not in hass.data[DOMAIN]["devices"] + ] + + for device in new_devices: + light = pykulersky.Light(device["address"], device["name"]) + try: + # Attempt to connect to this light and read the color. If the + # connection fails, either this is not a Kuler Sky light, or + # it's bluetooth connection is currently locked by another + # device. If the vendor's app is connected to the light when + # home assistant tries to connect, this connection will fail. + await hass.async_add_executor_job(light.connect) + await hass.async_add_executor_job(light.get_color) + except pykulersky.PykulerskyException: + continue + # The light has successfully connected + hass.data[DOMAIN]["devices"].add(device["address"]) + async_add_entities([KulerskyLight(light)], update_before_add=True) + + # Start initial discovery + hass.async_add_job(discover) + + # Perform recurring discovery of new devices + async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + + +class KulerskyLight(LightEntity): + """Representation of an Kuler Sky Light.""" + + def __init__(self, light: pykulersky.Light): + """Initialize a Kuler Sky light.""" + self._light = light + self._hs_color = None + self._brightness = None + self._white_value = None + self._available = True + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove( + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + await self.hass.async_add_executor_job(self.disconnect) + + def disconnect(self, *args) -> None: + """Disconnect the underlying device.""" + self._light.disconnect() + + @property + def name(self): + """Return the display name of this light.""" + return self._light.name + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._light.address + + @property + def device_info(self): + """Device info for this light.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Brightech", + } + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_KULERSKY + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def is_on(self): + """Return true if light is on.""" + return self._brightness > 0 or self._white_value > 0 + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + default_hs = (0, 0) if self._hs_color is None else self._hs_color + hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + + default_brightness = 0 if self._brightness is None else self._brightness + brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) + + default_white_value = 255 if self._white_value is None else self._white_value + white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value) + + if brightness == 0 and white_value == 0 and not kwargs: + # If the light would be off, and no additional parameters were + # passed, just turn the light on full brightness. + brightness = 255 + white_value = 255 + + rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + + self._light.set_color(*rgb, white_value) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.set_color(0, 0, 0, 0) + + def update(self): + """Fetch new state data for this light.""" + try: + if not self._light.connected: + self._light.connect() + # pylint: disable=invalid-name + r, g, b, w = self._light.get_color() + except pykulersky.PykulerskyException as exc: + if self._available: + _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) + self._available = False + return + if not self._available: + _LOGGER.info("Reconnected to %s", self.entity_id) + self._available = True + + hsv = color_util.color_RGB_to_hsv(r, g, b) + self._hs_color = hsv[:2] + self._brightness = int(round((hsv[2] / 100) * 255)) + self._white_value = w diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json new file mode 100644 index 0000000000..4f445e4fc1 --- /dev/null +++ b/homeassistant/components/kulersky/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "kulersky", + "name": "Kuler Sky", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kulersky", + "requirements": [ + "pykulersky==0.4.0" + ], + "codeowners": [ + "@emlove" + ] +} diff --git a/homeassistant/components/kulersky/strings.json b/homeassistant/components/kulersky/strings.json new file mode 100644 index 0000000000..ad8f0f41ae --- /dev/null +++ b/homeassistant/components/kulersky/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/kulersky/translations/en.json b/homeassistant/components/kulersky/translations/en.json new file mode 100644 index 0000000000..f05becffed --- /dev/null +++ b/homeassistant/components/kulersky/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a8e871aa02..833f11190b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -108,6 +108,7 @@ FLOWS = [ "juicenet", "kodi", "konnected", + "kulersky", "life360", "lifx", "local_ip", diff --git a/requirements_all.txt b/requirements_all.txt index cb93fc3e7c..725bc5f0c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1475,6 +1475,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df7cbdafc6..c3f8ed3c3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,6 +745,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.lastfm pylast==4.0.0 diff --git a/tests/components/kulersky/__init__.py b/tests/components/kulersky/__init__.py new file mode 100644 index 0000000000..2b723b28fb --- /dev/null +++ b/tests/components/kulersky/__init__.py @@ -0,0 +1 @@ +"""Tests for the Kuler Sky integration.""" diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py new file mode 100644 index 0000000000..59e3188fd7 --- /dev/null +++ b/tests/components/kulersky/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Kuler Sky config flow.""" +import pykulersky + +from homeassistant import config_entries, setup +from homeassistant.components.kulersky.config_flow import DOMAIN + +from tests.async_mock import patch + + +async def test_flow_success(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Kuler Sky" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_no_devices_found(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_flow_exceptions_caught(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + side_effect=pykulersky.PykulerskyException("TEST"), + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py new file mode 100644 index 0000000000..1b2472d7d7 --- /dev/null +++ b/tests/components/kulersky/test_light.py @@ -0,0 +1,315 @@ +"""Test the Kuler Sky lights.""" +import asyncio + +import pykulersky +import pytest + +from homeassistant import setup +from homeassistant.components.kulersky.light import DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + SCAN_INTERVAL, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +import homeassistant.util.dt as dt_util + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +async def mock_entry(hass): + """Create a mock light entity.""" + return MockConfigEntry(domain=DOMAIN) + + +@pytest.fixture +async def mock_light(hass, mock_entry): + """Create a mock light entity.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + light, "get_color", return_value=(0, 0, 0, 0) + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_connect.called + light.connected = True + + yield light + + +async def test_init(hass, mock_light): + """Test platform setup.""" + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object(hass.loop, "stop"), patch.object( + mock_light, "disconnect" + ) as mock_disconnect: + await hass.async_stop() + await hass.async_block_till_done() + + assert mock_disconnect.called + + +async def test_discovery_lock(hass, mock_entry): + """Test discovery lock.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + discovery_finished = None + first_discovery_started = asyncio.Event() + + async def mock_discovery(*args): + """Block to simulate multiple discovery calls while one still running.""" + nonlocal discovery_finished + if discovery_finished: + first_discovery_started.set() + await discovery_finished.wait() + return [] + + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.light.async_track_time_interval", + ) as mock_track_time_interval: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + with patch.object( + hass, "async_add_executor_job", side_effect=mock_discovery + ) as mock_run_discovery: + discovery_coroutine = mock_track_time_interval.call_args[0][1] + + discovery_finished = asyncio.Event() + + # Schedule multiple discoveries + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + + # Wait until the first discovery call is blocked + await first_discovery_started.wait() + + # Unblock the first discovery + discovery_finished.set() + + # Flush the remaining jobs + await hass.async_block_till_done() + + # The discovery method should only have been called once + mock_run_discovery.assert_called_once() + + +async def test_discovery_connection_error(hass, mock_entry): + """Test that invalid devices are skipped.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object( + light, "connect", side_effect=pykulersky.PykulerskyException + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert entity was not added + state = hass.states.get("light.bedroom") + assert state is None + + +async def test_remove_entry(hass, mock_light, mock_entry): + """Test platform setup.""" + with patch.object(mock_light, "disconnect") as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + +async def test_update_exception(hass, mock_light): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException + ): + await hass.helpers.entity_component.async_update_entity("light.bedroom") + state = hass.states.get("light.bedroom") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_light_turn_on(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(255, 255, 255, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(255, 255, 255, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 50, 50, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 50, 50, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 45, 25, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(50, 45, 25, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(220, 201, 110, 180) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 45, 25, 180) + + +async def test_light_turn_off(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(0, 0, 0, 0) + ): + await hass.services.async_call( + "light", + "turn_off", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(0, 0, 0, 0) + + +async def test_light_update(hass, mock_light): + """Test KulerSkyLight update.""" + utcnow = dt_util.utcnow() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + # Test an exception during discovery + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST") + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_UNAVAILABLE + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object( + mock_light, + "get_color", + return_value=(80, 160, 200, 240), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_ON + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + ATTR_BRIGHTNESS: 200, + ATTR_HS_COLOR: (200, 60), + ATTR_RGB_COLOR: (102, 203, 255), + ATTR_WHITE_VALUE: 240, + ATTR_XY_COLOR: (0.184, 0.261), + } From 8e6108b9e10492f098307fda8350928c21d07b32 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 3 Dec 2020 00:04:35 +0000 Subject: [PATCH 003/302] [ci skip] Translation update --- .../accuweather/translations/no.json | 6 +++ .../accuweather/translations/ru.json | 5 ++ .../components/airly/translations/no.json | 5 ++ .../components/airly/translations/ru.json | 5 ++ .../components/apple_tv/translations/no.json | 30 +++++++++++ .../components/apple_tv/translations/ru.json | 52 +++++++++++++++++++ .../components/braviatv/translations/no.json | 2 +- .../homematicip_cloud/translations/no.json | 4 +- .../components/kulersky/translations/no.json | 13 +++++ .../components/kulersky/translations/ru.json | 13 +++++ .../components/nest/translations/no.json | 4 +- .../components/ozw/translations/no.json | 5 ++ .../components/ozw/translations/ru.json | 5 ++ .../panasonic_viera/translations/no.json | 6 +-- .../components/ps4/translations/no.json | 6 +-- .../components/risco/translations/no.json | 6 +-- .../components/vizio/translations/no.json | 2 +- 17 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/apple_tv/translations/no.json create mode 100644 homeassistant/components/apple_tv/translations/ru.json create mode 100644 homeassistant/components/kulersky/translations/no.json create mode 100644 homeassistant/components/kulersky/translations/ru.json diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index 78a0d22878..50482cb3e6 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -31,5 +31,11 @@ "title": "AccuWeather-alternativer" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 AccuWeather-serveren", + "remaining_requests": "Gjenv\u00e6rende tillatte foresp\u00f8rsler" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 16623e4e70..1d90958efe 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -31,5 +31,10 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 AccuWeather" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index f0a657d33d..b38568210a 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -19,5 +19,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 Airly-serveren" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index a047d1e747..b1469af787 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json new file mode 100644 index 0000000000..27823ede2b --- /dev/null +++ b/homeassistant/components/apple_tv/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "unknown": "Uventet feil" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "invalid_auth": "Ugyldig godkjenning", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "unknown": "Uventet feil" + }, + "flow_title": "", + "step": { + "pair_with_pin": { + "data": { + "pin": "PIN kode" + } + }, + "user": { + "data": { + "device_input": "Enhet" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json new file mode 100644 index 0000000000..5c62b1f53e --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0435 \u0440\u0430\u0437.", + "no_devices_found": "\u041d\u0438\u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0441\u0435\u0442\u0438", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "invalid_auth": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "no_devices_found": "\u041d\u0438\u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0441\u0435\u0442\u0438", + "no_usable_service": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043d\u043e \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043d\u0435\u043c\u0443. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442\u0435 \u0432\u0438\u0434\u0435\u0442\u044c \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0435\u0433\u043e IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Apple TV.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + }, + "flow_title": "Apple TV", + "step": { + "confirm": { + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 Apple TV" + }, + "pair_with_pin": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "reconfigure": { + "description": "\u042d\u0442\u043e\u0442 Apple TV \u0438\u0441\u043f\u044b\u0442\u044b\u0432\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0442\u0440\u0443\u0434\u043d\u043e\u0441\u0442\u0438 \u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c, \u0438 \u0435\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "service_problem": { + "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443" + }, + "user": { + "data": { + "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438, \u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0438\u0436\u0435. \n\n \u0415\u0441\u043b\u0438 \u0432\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0438\u0441\u043f\u044b\u0442\u044b\u0432\u0430\u0435\u0442\u0435 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \n\n {devices}", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u041d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 Home Assistant" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index fc725b11ad..0c960a850e 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -12,7 +12,7 @@ "step": { "authorize": { "data": { - "pin": "PIN-kode" + "pin": "PIN kode" }, "description": "Angi PIN-koden som vises p\u00e5 Sony Bravia TV. \n\nHvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en, g\u00e5 til: Innstillinger -> Nettverk -> Innstillinger for ekstern enhet -> Avregistrere ekstern enhet.", "title": "Godkjenn Sony Bravia TV" diff --git a/homeassistant/components/homematicip_cloud/translations/no.json b/homeassistant/components/homematicip_cloud/translations/no.json index d28fe17a69..7d24da73e7 100644 --- a/homeassistant/components/homematicip_cloud/translations/no.json +++ b/homeassistant/components/homematicip_cloud/translations/no.json @@ -6,7 +6,7 @@ "unknown": "Uventet feil" }, "error": { - "invalid_sgtin_or_pin": "Ugyldig SGTIN eller PIN-kode , pr\u00f8v igjen.", + "invalid_sgtin_or_pin": "Ugyldig SGTIN eller PIN kode , pr\u00f8v igjen.", "press_the_button": "Vennligst trykk p\u00e5 den bl\u00e5 knappen.", "register_failed": "Kunne ikke registrere, vennligst pr\u00f8v igjen.", "timeout_button": "Bl\u00e5 knapp-trykk tok for lang tid, vennligst pr\u00f8v igjen." @@ -16,7 +16,7 @@ "data": { "hapid": "Tilgangspunkt ID (SGTIN)", "name": "Navn (valgfritt, brukes som navneprefiks for alle enheter)", - "pin": "PIN-kode" + "pin": "PIN kode" }, "title": "Velg HomematicIP tilgangspunkt" }, diff --git a/homeassistant/components/kulersky/translations/no.json b/homeassistant/components/kulersky/translations/no.json new file mode 100644 index 0000000000..b3d6b5d782 --- /dev/null +++ b/homeassistant/components/kulersky/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ru.json b/homeassistant/components/kulersky/translations/ru.json new file mode 100644 index 0000000000..46de86e5ec --- /dev/null +++ b/homeassistant/components/kulersky/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0438\u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u0423\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u0412\u043e\u0437\u043c\u043e\u0436\u043da \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u044b?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 69d67c5b4f..f1232b5f86 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -13,7 +13,7 @@ }, "error": { "internal_error": "Intern feil ved validering av kode", - "invalid_pin": "Ugyldig PIN-kode", + "invalid_pin": "Ugyldig PIN kode", "timeout": "Tidsavbrudd ved validering av kode", "unknown": "Uventet feil" }, @@ -27,7 +27,7 @@ }, "link": { "data": { - "code": "PIN-kode" + "code": "PIN kode" }, "description": "For \u00e5 koble din Nest-konto, [bekreft kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", "title": "Koble til Nest konto" diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json index 966e1a4065..89563ff353 100644 --- a/homeassistant/components/ozw/translations/no.json +++ b/homeassistant/components/ozw/translations/no.json @@ -4,6 +4,8 @@ "addon_info_failed": "Kunne ikke hente OpenZWave-tilleggsinfo", "addon_install_failed": "Kunne ikke installere OpenZWave-tillegget", "addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon", + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, @@ -14,6 +16,9 @@ "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter." }, "step": { + "hassio_confirm": { + "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegget" + }, "install_addon": { "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet" }, diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json index b2f5ebd6e8..129043728a 100644 --- a/homeassistant/components/ozw/translations/ru.json +++ b/homeassistant/components/ozw/translations/ru.json @@ -4,6 +4,8 @@ "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 OpenZWave.", "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c OpenZWave.", "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e OpenZWave.", + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, @@ -14,6 +16,9 @@ "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." }, "step": { + "hassio_confirm": { + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 OpenZWave \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c OpenZWave add-on." + }, "install_addon": { "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" }, diff --git a/homeassistant/components/panasonic_viera/translations/no.json b/homeassistant/components/panasonic_viera/translations/no.json index 7efa1e3176..5c0105a762 100644 --- a/homeassistant/components/panasonic_viera/translations/no.json +++ b/homeassistant/components/panasonic_viera/translations/no.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "invalid_pin_code": "PIN-kode du skrev inn var ugyldig" + "invalid_pin_code": "PIN kode du skrev inn var ugyldig" }, "step": { "pairing": { "data": { - "pin": "PIN-kode" + "pin": "PIN kode" }, - "description": "Skriv inn PIN-kode som vises p\u00e5 TV-en", + "description": "Skriv inn PIN kode som vises p\u00e5 TV-en", "title": "Sammenkobling" }, "user": { diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index f6f93fada0..185b0e031c 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "credential_timeout": "Legitimasjonstjenesten ble tidsavbrutt. Trykk send for \u00e5 starte p\u00e5 nytt.", - "login_failed": "Kunne ikke koble til PlayStation 4. Bekreft at PIN-kode er riktig.", + "login_failed": "Kunne ikke koble til PlayStation 4. Bekreft at PIN kode er riktig.", "no_ipaddress": "Skriv inn IP adresse til PlayStation 4 du vil konfigurere." }, "step": { @@ -20,12 +20,12 @@ }, "link": { "data": { - "code": "PIN-kode", + "code": "PIN kode", "ip_address": "IP adresse", "name": "Navn", "region": "" }, - "description": "Skriv inn PlayStation 4-informasjonen din. For PIN-kode , naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Innstillinger for tilkobling av mobilapp' og velg 'Legg til enhet'. Skriv inn PIN-kode som vises. Se [dokumentasjon] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", + "description": "Skriv inn PlayStation 4-informasjonen din. For PIN kode , naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Innstillinger for tilkobling av mobilapp' og velg 'Legg til enhet'. Skriv inn PIN kode som vises. Se [dokumentasjon] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", "title": "" }, "mode": { diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json index 5e57cd1464..758c5c68bb 100644 --- a/homeassistant/components/risco/translations/no.json +++ b/homeassistant/components/risco/translations/no.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Passord", - "pin": "PIN-kode", + "pin": "PIN kode", "username": "Brukernavn" } } @@ -32,8 +32,8 @@ }, "init": { "data": { - "code_arm_required": "Krev PIN-kode for \u00e5 tilkoble", - "code_disarm_required": "Krev PIN-kode for \u00e5 frakoble", + "code_arm_required": "Krev PIN kode for \u00e5 tilkoble", + "code_disarm_required": "Krev PIN kode for \u00e5 frakoble", "scan_interval": "Hvor ofte skal man unders\u00f8ke Risco (i l\u00f8pet av sekunder)" }, "title": "Konfigurer alternativer" diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index c5e0b6386b..de00cf0fce 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -13,7 +13,7 @@ "step": { "pair_tv": { "data": { - "pin": "PIN-kode" + "pin": "PIN kode" }, "description": "TVen skal vise en kode. Fyll inn denne koden i skjemaet, og fortsett deretter til neste trinn for \u00e5 fullf\u00f8re paringen.", "title": "Fullf\u00f8r sammenkoblingsprosess" From 4c7e17c5c68a64fab88d495893a700036d5b27bb Mon Sep 17 00:00:00 2001 From: tehbrd Date: Thu, 3 Dec 2020 10:58:10 +1000 Subject: [PATCH 004/302] Fix intesishome passing coroutine to HassJob (#43837) * Update climate.py Not allowed to pass coroutines to hassjob. * Update climate.py * Lint Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/intesishome/climate.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 57912d7d24..a41161c7a6 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -374,9 +374,11 @@ class IntesisAC(ClimateEntity): reconnect_minutes, ) # Schedule reconnection - async_call_later( - self.hass, reconnect_minutes * 60, self._controller.connect() - ) + + async def try_connect(_now): + await self._controller.connect() + + async_call_later(self.hass, reconnect_minutes * 60, try_connect) if self._controller.is_connected and not self._connected: # Connection has been restored From 69a438e2fc59555bc5120060f50364ccad7b4975 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 2 Dec 2020 20:45:08 -0700 Subject: [PATCH 005/302] Fix Slack "invalid_blocks_format" bug (#43875) * Fix Slack "invalid_blocks_format" bug * Fix optional params * Fix one more optional param * Update manifest --- CODEOWNERS | 1 + homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/slack/notify.py | 27 ++++++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c6deb8e9f8..27614c3d49 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -404,6 +404,7 @@ homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn +homeassistant/components/slack/* @bachya homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smappee/* @bsmappee diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index ad45abbe3c..e183dd455f 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -3,5 +3,5 @@ "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], - "codeowners": [] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 88317b3158..90caad62a5 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -198,17 +198,21 @@ class SlackNotificationService(BaseNotificationService): _LOGGER.error("Error while uploading file message: %s", err) async def _async_send_text_only_message( - self, targets, message, title, blocks, username, icon + self, + targets, + message, + title, + *, + username=None, + icon=None, + blocks=None, ): """Send a text-only message.""" - message_dict = { - "blocks": blocks, - "link_names": True, - "text": message, - "username": username, - } + message_dict = {"link_names": True, "text": message} + + if username: + message_dict["username"] = username - icon = icon or self._icon if icon: if icon.lower().startswith(("http://", "https://")): icon_type = "url" @@ -217,6 +221,9 @@ class SlackNotificationService(BaseNotificationService): message_dict[f"icon_{icon_type}"] = icon + if blocks: + message_dict["blocks"] = blocks + tasks = { target: self._client.chat_postMessage(**message_dict, channel=target) for target in targets @@ -256,15 +263,15 @@ class SlackNotificationService(BaseNotificationService): elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: - blocks = {} + blocks = None return await self._async_send_text_only_message( targets, message, title, - blocks, username=data.get(ATTR_USERNAME, self._username), icon=data.get(ATTR_ICON, self._icon), + blocks=blocks, ) # Message Type 2: A message that uploads a remote file From 40408eb0eb14a923e0a8f355609d6a6b754ecf27 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 3 Dec 2020 06:56:05 +0100 Subject: [PATCH 006/302] Add HmIP-HDM1 and HmIPW-DRD3 to Homematic IP Cloud (#43132) * cleanup const.py * Add wired multi dimmer HMIPW-DRD3 to Homematic IP Cloud * Add HmIP-HDM1 to Homematic IP Cloud --- .../components/homematicip_cloud/const.py | 27 +++-- .../components/homematicip_cloud/cover.py | 81 +++++++++++++- .../components/homematicip_cloud/light.py | 43 ++++++++ .../homematicip_cloud/test_cover.py | 103 ++++++++++++++++++ .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_light.py | 52 +++++++++ tests/fixtures/homematicip_cloud.json | 2 +- 7 files changed, 299 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 5c48de975f..4fb21febb4 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -1,19 +1,30 @@ """Constants for the HomematicIP Cloud component.""" import logging +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + _LOGGER = logging.getLogger(".") DOMAIN = "homematicip_cloud" COMPONENTS = [ - "alarm_control_panel", - "binary_sensor", - "climate", - "cover", - "light", - "sensor", - "switch", - "weather", + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + COVER_DOMAIN, + LIGHT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + WEATHER_DOMAIN, ] CONF_ACCESSPOINT = "accesspoint" diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 60d3867d05..3d5af9b7c4 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -2,6 +2,7 @@ from typing import Optional from homematicip.aio.device import ( + AsyncBlindModule, AsyncFullFlushBlind, AsyncFullFlushShutter, AsyncGarageDoorModuleTormatic, @@ -34,7 +35,9 @@ async def async_setup_entry( hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: - if isinstance(device, AsyncFullFlushBlind): + if isinstance(device, AsyncBlindModule): + entities.append(HomematicipBlindModule(hap, device)) + elif isinstance(device, AsyncFullFlushBlind): entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): entities.append(HomematicipCoverShutter(hap, device)) @@ -51,6 +54,82 @@ async def async_setup_entry( async_add_entities(entities) +class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): + """Representation of the HomematicIP blind module.""" + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + if self._device.primaryShadingLevel is not None: + return int((1 - self._device.primaryShadingLevel) * 100) + return None + + @property + def current_cover_tilt_position(self) -> int: + """Return current tilt position of cover.""" + if self._device.secondaryShadingLevel is not None: + return int((1 - self._device.secondaryShadingLevel) * 100) + return None + + async def async_set_cover_position(self, **kwargs) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + # HmIP cover is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_primary_shading_level(primaryShadingLevel=level) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Move the cover to a specific tilt position.""" + position = kwargs[ATTR_TILT_POSITION] + # HmIP slats is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=level, + ) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + if self._device.primaryShadingLevel is not None: + return self._device.primaryShadingLevel == HMIP_COVER_CLOSED + return None + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.set_primary_shading_level( + primaryShadingLevel=HMIP_COVER_OPEN + ) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.set_primary_shading_level( + primaryShadingLevel=HMIP_COVER_CLOSED + ) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the device if in motion.""" + await self._device.stop() + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open the slats.""" + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=HMIP_SLATS_OPEN, + ) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close the slats.""" + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=HMIP_SLATS_CLOSED, + ) + + async def async_stop_cover_tilt(self, **kwargs) -> None: + """Stop the device if in motion.""" + await self._device.stop() + + class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): """Representation of the HomematicIP cover shutter.""" diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index f387e7bfda..f0c191ac1a 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -8,6 +8,7 @@ from homematicip.aio.device import ( AsyncDimmer, AsyncFullFlushDimmer, AsyncPluggableDimmer, + AsyncWiredDimmer3, ) from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel @@ -51,6 +52,9 @@ async def async_setup_entry( hap, device, device.bottomLightChannelIndex ) ) + elif isinstance(device, AsyncWiredDimmer3): + for channel in range(1, 4): + entities.append(HomematicipMultiDimmer(hap, device, channel=channel)) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), @@ -99,6 +103,45 @@ class HomematicipLightMeasuring(HomematicipLight): return state_attr +class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): + """Representation of HomematicIP Cloud dimmer.""" + + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + """Initialize the dimmer light entity.""" + super().__init__(hap, device, channel=channel) + + @property + def is_on(self) -> bool: + """Return true if dimmer is on.""" + func_channel = self._device.functionalChannels[self._channel] + return func_channel.dimLevel is not None and func_channel.dimLevel > 0.0 + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return int( + (self._device.functionalChannels[self._channel].dimLevel or 0.0) * 255 + ) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs) -> None: + """Turn the dimmer on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._device.set_dim_level( + kwargs[ATTR_BRIGHTNESS] / 255.0, self._channel + ) + else: + await self._device.set_dim_level(1, self._channel) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the dimmer off.""" + await self._device.set_dim_level(0, self._channel) + + class HomematicipDimmer(HomematicipGenericEntity, LightEntity): """Representation of HomematicIP Cloud dimmer.""" diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 7ef0e3d670..82d2f41de5 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -160,6 +160,109 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): assert ha_state.state == STATE_UNKNOWN +async def test_hmip_blind_module(hass, default_mock_hap_factory): + """Test HomematicipBlindModule.""" + entity_id = "cover.sonnenschutz_balkontur" + entity_name = "Sonnenschutz Balkontür" + device_model = "HmIP-HDM1" + 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, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 5 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_secondary_shading_level" + assert hmip_device.mock_calls[-1][2] == { + "primaryShadingLevel": 0.94956, + "secondaryShadingLevel": 0, + } + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 0) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 0) + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + + assert hmip_device.mock_calls[-1][0] == "set_primary_shading_level" + assert hmip_device.mock_calls[-1][2] == {"primaryShadingLevel": 0} + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 0.5) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 0.5) + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + + assert hmip_device.mock_calls[-1][0] == "set_primary_shading_level" + assert hmip_device.mock_calls[-1][2] == {"primaryShadingLevel": 0.5} + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 1) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 1) + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 12 + + assert hmip_device.mock_calls[-1][0] == "set_secondary_shading_level" + assert hmip_device.mock_calls[-1][2] == { + "primaryShadingLevel": 1, + "secondaryShadingLevel": 1, + } + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 13 + assert hmip_device.mock_calls[-1][0] == "stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", None) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN + + async def test_hmip_garage_door_tormatic(hass, default_mock_hap_factory): """Test HomematicipCoverShutte.""" entity_id = "cover.garage_door_module" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 31e62a1a71..4047a8ef28 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 233 + assert len(mock_hap.hmip_device_by_entity_id) == 236 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 8ab62019c3..b62a98fd03 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -245,3 +245,55 @@ async def test_hmip_light_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + + +async def test_hmip_wired_multi_dimmer(hass, default_mock_hap_factory): + """Test HomematicipMultiDimmer.""" + entity_id = "light.raumlich_kuche" + entity_name = "Raumlich (Küche)" + device_model = "HmIPW-DRD3" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wired Dimmaktor – 3-fach (Küche)"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1, 1) + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness": "100"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 2 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0.39215686274509803, 1) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0, 1) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 9c2a1b1e37..7adf4b85b1 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -233,7 +233,7 @@ "profileMode": "AUTOMATIC", "secondaryCloseAdjustable": false, "secondaryOpenAdjustable": false, - "secondaryShadingLevel": null, + "secondaryShadingLevel": 0, "secondaryShadingStateType": "NOT_EXISTENT", "shadingDriveVersion": null, "shadingPackagePosition": "TOP", From 6f2327c6d574f7af4b6f495bd9439e8a91136b9e Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 3 Dec 2020 09:10:20 +0100 Subject: [PATCH 007/302] Change config flow unique_id for devolo Home Control (#43005) --- .../devolo_home_control/__init__.py | 21 +++++++++++++------ .../devolo_home_control/config_flow.py | 4 ++-- .../components/devolo_home_control/const.py | 2 ++ .../devolo_home_control/test_config_flow.py | 14 ++++++------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 96d52b57e8..e5ee902930 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTAN from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_MYDEVOLO, DOMAIN, PLATFORMS +from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS async def async_setup(hass, config): @@ -22,13 +22,9 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the devolo account from a config entry.""" - conf = entry.data hass.data.setdefault(DOMAIN, {}) - mydevolo = Mydevolo() - mydevolo.user = conf[CONF_USERNAME] - mydevolo.password = conf[CONF_PASSWORD] - mydevolo.url = conf[CONF_MYDEVOLO] + mydevolo = _mydevolo(entry.data) credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) @@ -40,6 +36,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids) + if GATEWAY_SERIAL_PATTERN.match(entry.unique_id): + uuid = await hass.async_add_executor_job(mydevolo.uuid) + hass.config_entries.async_update_entry(entry, unique_id=uuid) + try: zeroconf_instance = await zeroconf.async_get_instance(hass) hass.data[DOMAIN][entry.entry_id] = {"gateways": [], "listener": None} @@ -95,3 +95,12 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo hass.data[DOMAIN][entry.entry_id]["listener"]() hass.data[DOMAIN].pop(entry.entry_id) return unload + + +def _mydevolo(conf: dict) -> Mydevolo: + """Configure mydevolo.""" + mydevolo = Mydevolo() + mydevolo.user = conf[CONF_USERNAME] + mydevolo.password = conf[CONF_PASSWORD] + mydevolo.url = conf[CONF_MYDEVOLO] + return mydevolo diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 67803ec56b..3f51a9c088 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -55,8 +55,8 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not credentials_valid: return self._show_form({"base": "invalid_auth"}) _LOGGER.debug("Credentials valid") - gateway_ids = await self.hass.async_add_executor_job(mydevolo.get_gateway_ids) - await self.async_set_unique_id(gateway_ids[0]) + uuid = await self.hass.async_add_executor_job(mydevolo.uuid) + await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() return self.async_create_entry( diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index ea46ea4484..3a7d26435f 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -1,6 +1,8 @@ """Constants for the devolo_home_control integration.""" +import re DOMAIN = "devolo_home_control" DEFAULT_MYDEVOLO = "https://www.mydevolo.com" PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"] CONF_MYDEVOLO = "mydevolo_url" +GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}") diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index ad99f13e8f..060188d65a 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -26,8 +26,8 @@ async def test_form(hass): "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", return_value=True, ), patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", - return_value=["123456"], + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + return_value="123456", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,13 +71,13 @@ async def test_form_invalid_credentials(hass): async def test_form_already_configured(hass): """Test if we get the error message on already configured.""" with patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", - return_value=["1234567"], + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + return_value="123456", ), patch( "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", return_value=True, ): - MockConfigEntry(domain=DOMAIN, unique_id="1234567", data={}).add_to_hass(hass) + MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -105,8 +105,8 @@ async def test_form_advanced_options(hass): "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", return_value=True, ), patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", - return_value=["123456"], + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + return_value="123456", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From 2c66d26415b6e5d6ae9859ddc1d1266491e6e88b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 3 Dec 2020 10:37:39 +0100 Subject: [PATCH 008/302] Improve look up speed by inverting dictionaries (#43883) --- homeassistant/components/deconz/climate.py | 70 ++++++++++------------ 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 3e1e174873..98e3864e19 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -32,7 +32,7 @@ from .gateway import get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" -FAN_MODES = { +FAN_MODE_TO_DECONZ = { DECONZ_FAN_SMART: "smart", FAN_AUTO: "auto", FAN_HIGH: "high", @@ -42,7 +42,9 @@ FAN_MODES = { FAN_OFF: "off", } -HVAC_MODES = { +DECONZ_TO_FAN_MODE = {value: key for key, value in FAN_MODE_TO_DECONZ.items()} + +HVAC_MODE_TO_DECONZ = { HVAC_MODE_AUTO: "auto", HVAC_MODE_COOL: "cool", HVAC_MODE_HEAT: "heat", @@ -54,7 +56,7 @@ DECONZ_PRESET_COMPLEX = "complex" DECONZ_PRESET_HOLIDAY = "holiday" DECONZ_PRESET_MANUAL = "manual" -PRESET_MODES = { +PRESET_MODE_TO_DECONZ = { DECONZ_PRESET_AUTO: "auto", PRESET_BOOST: "boost", PRESET_COMFORT: "comfort", @@ -64,6 +66,8 @@ PRESET_MODES = { DECONZ_PRESET_MANUAL: "manual", } +DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ climate devices. @@ -111,14 +115,17 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Set up thermostat device.""" super().__init__(device, gateway) - self._hvac_modes = dict(HVAC_MODES) + self._hvac_mode_to_deconz = dict(HVAC_MODE_TO_DECONZ) if "mode" not in device.raw["config"]: - self._hvac_modes = { + self._hvac_mode_to_deconz = { HVAC_MODE_HEAT: True, HVAC_MODE_OFF: False, } elif "coolsetpoint" not in device.raw["config"]: - self._hvac_modes.pop(HVAC_MODE_COOL) + self._hvac_mode_to_deconz.pop(HVAC_MODE_COOL) + self._deconz_to_hvac_mode = { + value: key for key, value in self._hvac_mode_to_deconz.items() + } self._features = SUPPORT_TARGET_TEMPERATURE @@ -138,26 +145,21 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def fan_mode(self) -> str: """Return fan operation.""" - for hass_fan_mode, fan_mode in FAN_MODES.items(): - if self._device.fanmode == fan_mode: - return hass_fan_mode - - if self._device.state_on: - return FAN_ON - - return FAN_OFF + return DECONZ_TO_FAN_MODE.get( + self._device.fanmode, FAN_ON if self._device.state_on else FAN_OFF + ) @property def fan_modes(self) -> list: """Return the list of available fan operation modes.""" - return list(FAN_MODES) + return list(FAN_MODE_TO_DECONZ) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if fan_mode not in FAN_MODES: + if fan_mode not in FAN_MODE_TO_DECONZ: raise ValueError(f"Unsupported fan mode {fan_mode}") - data = {"fanmode": FAN_MODES[fan_mode]} + data = {"fanmode": FAN_MODE_TO_DECONZ[fan_mode]} await self._device.async_set_config(data) @@ -169,28 +171,24 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): Need to be one of HVAC_MODE_*. """ - for hass_hvac_mode, device_mode in self._hvac_modes.items(): - if self._device.mode == device_mode: - return hass_hvac_mode - - if self._device.state_on: - return HVAC_MODE_HEAT - - return HVAC_MODE_OFF + return self._deconz_to_hvac_mode.get( + self._device.mode, + HVAC_MODE_HEAT if self._device.state_on else HVAC_MODE_OFF, + ) @property def hvac_modes(self) -> list: """Return the list of available hvac operation modes.""" - return list(self._hvac_modes) + return list(self._hvac_mode_to_deconz) async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if hvac_mode not in self._hvac_modes: + if hvac_mode not in self._hvac_mode_to_deconz: raise ValueError(f"Unsupported HVAC mode {hvac_mode}") - data = {"mode": self._hvac_modes[hvac_mode]} - if len(self._hvac_modes) == 2: # Only allow turn on and off thermostat - data = {"on": self._hvac_modes[hvac_mode]} + data = {"mode": self._hvac_mode_to_deconz[hvac_mode]} + if len(self._hvac_mode_to_deconz) == 2: # Only allow turn on and off thermostat + data = {"on": self._hvac_mode_to_deconz[hvac_mode]} await self._device.async_set_config(data) @@ -199,23 +197,19 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def preset_mode(self) -> Optional[str]: """Return preset mode.""" - for hass_preset_mode, preset_mode in PRESET_MODES.items(): - if self._device.preset == preset_mode: - return hass_preset_mode - - return None + return DECONZ_TO_PRESET_MODE.get(self._device.preset) @property def preset_modes(self) -> list: """Return the list of available preset modes.""" - return list(PRESET_MODES) + return list(PRESET_MODE_TO_DECONZ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in PRESET_MODES: + if preset_mode not in PRESET_MODE_TO_DECONZ: raise ValueError(f"Unsupported preset mode {preset_mode}") - data = {"preset": PRESET_MODES[preset_mode]} + data = {"preset": PRESET_MODE_TO_DECONZ[preset_mode]} await self._device.async_set_config(data) From 78a69ef2849baf6e0be4e34fdb9beb9bcdba98e8 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Thu, 3 Dec 2020 12:39:50 +0300 Subject: [PATCH 009/302] Add reproduce state for Number (#43870) --- .../components/number/reproduce_state.py | 65 +++++++++++++++++++ .../components/number/test_reproduce_state.py | 53 +++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 homeassistant/components/number/reproduce_state.py create mode 100644 tests/components/number/test_reproduce_state.py diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py new file mode 100644 index 0000000000..611744e319 --- /dev/null +++ b/homeassistant/components/number/reproduce_state.py @@ -0,0 +1,65 @@ +"""Reproduce a Number entity state.""" +import asyncio +import logging +from typing import Any, Dict, Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, + state: State, + *, + context: Optional[Context] = None, + reproduce_options: Optional[Dict[str, Any]] = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + try: + float(state.state) + except ValueError: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, + states: Iterable[State], + *, + context: Optional[Context] = None, + reproduce_options: Optional[Dict[str, Any]] = None, +) -> None: + """Reproduce multiple Number states.""" + # Reproduce states in parallel. + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/tests/components/number/test_reproduce_state.py b/tests/components/number/test_reproduce_state.py new file mode 100644 index 0000000000..654f87cbce --- /dev/null +++ b/tests/components/number/test_reproduce_state.py @@ -0,0 +1,53 @@ +"""Test reproduce state for Number entities.""" +from homeassistant.components.number.const import ( + ATTR_MAX, + ATTR_MIN, + DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.core import State + +from tests.common import async_mock_service + +VALID_NUMBER1 = "19.0" +VALID_NUMBER2 = "99.9" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Number states.""" + + hass.states.async_set( + "number.test_number", VALID_NUMBER1, {ATTR_MIN: 5, ATTR_MAX: 100} + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("number.test_number", VALID_NUMBER1), + # Should not raise + State("number.non_existing", "234"), + ], + ) + + assert hass.states.get("number.test_number").state == VALID_NUMBER1 + + # Test reproducing with different state + calls = async_mock_service(hass, DOMAIN, SERVICE_SET_VALUE) + await hass.helpers.state.async_reproduce_state( + [ + State("number.test_number", VALID_NUMBER2), + # Should not raise + State("number.non_existing", "234"), + ], + ) + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].data == {"entity_id": "number.test_number", "value": VALID_NUMBER2} + + # Test invalid state + await hass.helpers.state.async_reproduce_state( + [State("number.test_number", "invalid_state")] + ) + + assert len(calls) == 1 From 262e77d96970664b78c0a08d46f4bf19fafef51e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Dec 2020 16:44:18 +0100 Subject: [PATCH 010/302] Blueprint: descriptions + descriptive errors (#43899) --- .../components/automation/blueprints/motion_light.yaml | 3 ++- .../automation/blueprints/notify_leaving_zone.yaml | 4 +++- homeassistant/components/blueprint/importer.py | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index c10d3691e6..c11d22d974 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -1,5 +1,6 @@ blueprint: name: Motion-activated Light + description: Turn on a light when motion is detected. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml input: @@ -17,7 +18,7 @@ blueprint: domain: light no_motion_wait: name: Wait time - description: Time to wait until the light should be turned off. + description: Time to leave the light on after last motion is detected. default: 120 selector: number: diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 9b79396f06..d3a70d773e 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -1,5 +1,6 @@ blueprint: - name: Send notification when a person leaves a zone + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml input: @@ -26,6 +27,7 @@ trigger: variables: zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. zone_state: "{{ states[zone_entity].name }}" person_entity: !input person_entity person_name: "{{ states[person_entity].name }}" diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index bc40f76e7c..524b04293e 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -124,7 +124,9 @@ def _extract_blueprint_from_community_topic( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the topic") + raise HomeAssistantError( + "No valid blueprint found in the topic. Blueprint syntax blocks need to be marked as YAML or no syntax." + ) return ImportedBlueprint( f'{post["username"]}/{topic["slug"]}', block_content, blueprint @@ -204,7 +206,9 @@ async def fetch_blueprint_from_github_gist_url( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the gist") + raise HomeAssistantError( + "No valid blueprint found in the gist. The blueprint file needs to end with '.yaml'" + ) return ImportedBlueprint( f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint From 54cb2d42afb5563fbc28110b2181d9deb96308ed Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Thu, 3 Dec 2020 11:08:16 -0600 Subject: [PATCH 011/302] Kulersky cleanups (#43901) --- .../components/kulersky/config_flow.py | 2 +- homeassistant/components/kulersky/light.py | 21 ++++++++++++------- tests/components/kulersky/test_light.py | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py index 2b22fcdbd3..04f7719b8e 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -25,5 +25,5 @@ async def _async_has_devices(hass) -> bool: config_entry_flow.register_discovery_flow( - DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 4c17d1bcba..71dd4a158c 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -33,6 +33,12 @@ DISCOVERY_INTERVAL = timedelta(seconds=60) PARALLEL_UPDATES = 0 +def check_light(light: pykulersky.Light): + """Attempt to connect to this light and read the color.""" + light.connect() + light.get_color() + + async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, @@ -69,13 +75,12 @@ async def async_setup_entry( for device in new_devices: light = pykulersky.Light(device["address"], device["name"]) try: - # Attempt to connect to this light and read the color. If the - # connection fails, either this is not a Kuler Sky light, or - # it's bluetooth connection is currently locked by another - # device. If the vendor's app is connected to the light when - # home assistant tries to connect, this connection will fail. - await hass.async_add_executor_job(light.connect) - await hass.async_add_executor_job(light.get_color) + # If the connection fails, either this is not a Kuler Sky + # light, or it's bluetooth connection is currently locked + # by another device. If the vendor's app is connected to + # the light when home assistant tries to connect, this + # connection will fail. + await hass.async_add_executor_job(check_light, light) except pykulersky.PykulerskyException: continue # The light has successfully connected @@ -83,7 +88,7 @@ async def async_setup_entry( async_add_entities([KulerskyLight(light)], update_before_add=True) # Start initial discovery - hass.async_add_job(discover) + hass.async_create_task(discover()) # Perform recurring discovery of new devices async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 1b2472d7d7..5403f7cedd 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -56,11 +56,11 @@ async def mock_light(hass, mock_entry): ], ): with patch( - "homeassistant.components.kulersky.light.pykulersky.Light" - ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + "homeassistant.components.kulersky.light.pykulersky.Light", + return_value=light, + ), patch.object(light, "connect") as mock_connect, patch.object( light, "get_color", return_value=(0, 0, 0, 0) ): - mockdevice.return_value = light mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From 8d33c2092f485d4a90cf691ce9e55ae242020659 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 3 Dec 2020 18:35:17 +0100 Subject: [PATCH 012/302] Add number entity value property (#43902) --- homeassistant/components/demo/number.py | 2 +- homeassistant/components/number/__init__.py | 11 +++++++++++ tests/components/number/test_init.py | 21 +++++++++++++++------ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index a8b9cb0ac4..5a6ce5f5c6 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -98,7 +98,7 @@ class DemoNumber(NumberEntity): return self._assumed @property - def state(self): + def value(self): """Return the current value.""" return self._state diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 2fd04943e4..31a0bcd776 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,4 +1,5 @@ """Component to allow numeric input for platforms.""" +from abc import abstractmethod from datetime import timedelta import logging from typing import Any, Dict @@ -93,6 +94,16 @@ class NumberEntity(Entity): step /= 10.0 return step + @property + def state(self) -> float: + """Return the entity state.""" + return self.value + + @property + @abstractmethod + def value(self) -> float: + """Return the entity value to represent the entity state.""" + def set_value(self, value: float) -> None: """Set new value.""" raise NotImplementedError() diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 6037bde5af..58e090db20 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,8 +1,17 @@ """The tests for the Number component.""" -from unittest.mock import MagicMock - from homeassistant.components.number import NumberEntity +from tests.async_mock import MagicMock + + +class MockDefaultNumberEntity(NumberEntity): + """Mock NumberEntity device to use in tests.""" + + @property + def value(self): + """Return the current value.""" + return 0.5 + class MockNumberEntity(NumberEntity): """Mock NumberEntity device to use in tests.""" @@ -13,14 +22,14 @@ class MockNumberEntity(NumberEntity): return 1.0 @property - def state(self): + def value(self): """Return the current value.""" - return "0.5" + return 0.5 async def test_step(hass): """Test the step calculation.""" - number = NumberEntity() + number = MockDefaultNumberEntity() assert number.step == 1.0 number_2 = MockNumberEntity() @@ -29,7 +38,7 @@ async def test_step(hass): async def test_sync_set_value(hass): """Test if async set_value calls sync set_value.""" - number = NumberEntity() + number = MockDefaultNumberEntity() number.hass = hass number.set_value = MagicMock() From 5742db630851cb7b4c4c1d6f8cbc4c58bde86b8c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 3 Dec 2020 19:40:33 +0100 Subject: [PATCH 013/302] Unsubscribe ozw stop listener on entry unload (#43900) --- homeassistant/components/ozw/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index c0d50e18ab..1f46e7a17c 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -279,7 +279,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except asyncio.CancelledError: pass - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client) + ozw_data[DATA_UNSUBSCRIBE].append( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client + ) + ) ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client else: From 4ef93feb9a47b349195f46299580171a56b58e06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Dec 2020 09:02:18 -1000 Subject: [PATCH 014/302] Bump icmplib to 2.0 for ping (#43868) --- homeassistant/components/ping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 33788960de..258a75caa0 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,6 +3,6 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], - "requirements": ["icmplib==1.2.2"], + "requirements": ["icmplib==2.0"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 725bc5f0c0..3a906d8344 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -810,7 +810,7 @@ ibm-watson==4.0.1 ibmiotf==0.3.4 # homeassistant.components.ping -icmplib==1.2.2 +icmplib==2.0 # homeassistant.components.iglo iglo==1.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3f8ed3c3b..42d1319163 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -419,7 +419,7 @@ hyperion-py==0.6.0 iaqualink==0.3.4 # homeassistant.components.ping -icmplib==1.2.2 +icmplib==2.0 # homeassistant.components.influxdb influxdb-client==1.8.0 From 44d7787582e32917ecc49d879947aa44fb82076a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Dec 2020 22:41:02 +0100 Subject: [PATCH 015/302] Updated frontend to 20201203.0 (#43907) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 34cd7edbf2..1bf6cdc580 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201202.0"], + "requirements": ["home-assistant-frontend==20201203.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f85297fc7..65f228f5a0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3a906d8344..55fc295705 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42d1319163..b100b4fe96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 42f00cff304db35de5fdce56cdfcc41c00d39084 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 4 Dec 2020 00:05:42 +0000 Subject: [PATCH 016/302] [ci skip] Translation update --- .../components/abode/translations/nl.json | 8 ++- .../abode/translations/zh-Hant.json | 2 +- .../accuweather/translations/et.json | 2 +- .../accuweather/translations/zh-Hans.json | 8 +++ .../accuweather/translations/zh-Hant.json | 8 ++- .../acmeda/translations/zh-Hant.json | 2 +- .../adguard/translations/zh-Hant.json | 2 +- .../advantage_air/translations/zh-Hant.json | 2 +- .../agent_dvr/translations/zh-Hant.json | 2 +- .../components/airly/translations/et.json | 2 +- .../airly/translations/zh-Hans.json | 5 ++ .../airly/translations/zh-Hant.json | 5 ++ .../airvisual/translations/zh-Hant.json | 2 +- .../alarmdecoder/translations/et.json | 4 +- .../alarmdecoder/translations/zh-Hant.json | 6 +- .../almond/translations/zh-Hant.json | 2 +- .../ambient_station/translations/zh-Hant.json | 2 +- .../components/apple_tv/translations/cs.json | 29 +++++++++ .../components/apple_tv/translations/nl.json | 9 +++ .../components/apple_tv/translations/pl.json | 20 ++++++ .../components/apple_tv/translations/ru.json | 26 +++++--- .../apple_tv/translations/zh-Hant.json | 64 +++++++++++++++++++ .../arcam_fmj/translations/zh-Hant.json | 4 +- .../components/atag/translations/zh-Hant.json | 6 +- .../awair/translations/zh-Hant.json | 2 +- .../components/axis/translations/zh-Hant.json | 12 ++-- .../blebox/translations/zh-Hant.json | 10 +-- .../blink/translations/zh-Hant.json | 2 +- .../components/bond/translations/zh-Hant.json | 4 +- .../braviatv/translations/zh-Hant.json | 2 +- .../broadlink/translations/zh-Hant.json | 18 +++--- .../brother/translations/zh-Hant.json | 2 +- .../bsblan/translations/zh-Hant.json | 6 +- .../canary/translations/zh-Hant.json | 2 +- .../components/cast/translations/zh-Hant.json | 4 +- .../components/cloud/translations/et.json | 6 +- .../cloudflare/translations/zh-Hant.json | 2 +- .../control4/translations/zh-Hant.json | 2 +- .../coolmaster/translations/zh-Hant.json | 2 +- .../daikin/translations/zh-Hant.json | 4 +- .../deconz/translations/zh-Hant.json | 44 ++++++------- .../denonavr/translations/zh-Hant.json | 2 +- .../device_tracker/translations/zh-Hant.json | 2 +- .../dialogflow/translations/et.json | 2 +- .../dialogflow/translations/zh-Hant.json | 2 +- .../directv/translations/zh-Hant.json | 2 +- .../doorbird/translations/zh-Hant.json | 6 +- .../components/dsmr/translations/zh-Hant.json | 2 +- .../dunehd/translations/zh-Hant.json | 4 +- .../components/eafm/translations/zh-Hant.json | 2 +- .../ecobee/translations/zh-Hant.json | 2 +- .../elgato/translations/zh-Hant.json | 6 +- .../emulated_roku/translations/zh-Hant.json | 2 +- .../enocean/translations/zh-Hant.json | 14 ++-- .../esphome/translations/zh-Hant.json | 2 +- .../components/flo/translations/zh-Hant.json | 2 +- .../forked_daapd/translations/zh-Hant.json | 6 +- .../freebox/translations/zh-Hant.json | 2 +- .../fritzbox/translations/zh-Hant.json | 6 +- .../geofency/translations/zh-Hant.json | 2 +- .../glances/translations/zh-Hant.json | 2 +- .../goalzero/translations/zh-Hant.json | 2 +- .../gpslogger/translations/zh-Hant.json | 2 +- .../components/gree/translations/zh-Hant.json | 4 +- .../guardian/translations/zh-Hant.json | 6 +- .../harmony/translations/zh-Hant.json | 2 +- .../hassio/translations/zh-Hans.json | 2 +- .../components/heos/translations/zh-Hant.json | 4 +- .../hisense_aehw4a1/translations/zh-Hant.json | 4 +- .../hlk_sw16/translations/zh-Hant.json | 2 +- .../homekit/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 34 +++++----- .../translations/zh-Hant.json | 4 +- .../huawei_lte/translations/zh-Hant.json | 10 +-- .../components/hue/translations/zh-Hant.json | 4 +- .../translations/zh-Hant.json | 2 +- .../hvv_departures/translations/zh-Hant.json | 2 +- .../components/hyperion/translations/nl.json | 11 ++++ .../iaqualink/translations/zh-Hant.json | 2 +- .../icloud/translations/zh-Hant.json | 10 +-- .../components/ifttt/translations/et.json | 2 +- .../ifttt/translations/zh-Hant.json | 2 +- .../components/insteon/translations/nl.json | 49 ++++++++++++-- .../insteon/translations/zh-Hant.json | 32 +++++----- .../components/ios/translations/zh-Hant.json | 2 +- .../components/ipma/translations/zh-Hans.json | 5 ++ .../components/ipp/translations/zh-Hant.json | 4 +- .../translations/zh-Hant.json | 2 +- .../isy994/translations/zh-Hant.json | 4 +- .../izone/translations/zh-Hant.json | 4 +- .../components/kodi/translations/zh-Hant.json | 2 +- .../konnected/translations/zh-Hant.json | 10 +-- .../components/kulersky/translations/cs.json | 13 ++++ .../components/kulersky/translations/et.json | 13 ++++ .../components/kulersky/translations/ru.json | 6 +- .../kulersky/translations/zh-Hant.json | 13 ++++ .../components/lifx/translations/zh-Hant.json | 4 +- .../local_ip/translations/zh-Hant.json | 2 +- .../locative/translations/zh-Hant.json | 2 +- .../components/lovelace/translations/et.json | 4 +- .../lovelace/translations/zh-Hans.json | 6 +- .../lutron_caseta/translations/zh-Hant.json | 2 +- .../mailgun/translations/zh-Hant.json | 2 +- .../mikrotik/translations/zh-Hant.json | 2 +- .../monoprice/translations/zh-Hant.json | 4 +- .../motion_blinds/translations/zh-Hant.json | 2 +- .../components/mqtt/translations/et.json | 4 +- .../components/mqtt/translations/zh-Hant.json | 2 +- .../neato/translations/zh-Hant.json | 2 +- .../components/nest/translations/nl.json | 8 +++ .../components/nest/translations/zh-Hant.json | 2 +- .../components/netatmo/translations/nl.json | 7 ++ .../netatmo/translations/zh-Hant.json | 2 +- .../nexia/translations/zh-Hant.json | 2 +- .../nightscout/translations/zh-Hant.json | 4 +- .../components/notion/translations/nl.json | 1 + .../notion/translations/zh-Hant.json | 2 +- .../nuheat/translations/zh-Hant.json | 2 +- .../components/nut/translations/zh-Hant.json | 2 +- .../components/nzbget/translations/nl.json | 9 +++ .../nzbget/translations/zh-Hant.json | 2 +- .../components/omnilogic/translations/nl.json | 9 +++ .../omnilogic/translations/zh-Hant.json | 2 +- .../components/onewire/translations/nl.json | 14 ++++ .../onewire/translations/zh-Hant.json | 4 +- .../onvif/translations/zh-Hant.json | 20 +++--- .../opentherm_gw/translations/zh-Hant.json | 2 +- .../ovo_energy/translations/nl.json | 3 + .../ovo_energy/translations/zh-Hant.json | 2 +- .../owntracks/translations/zh-Hant.json | 2 +- .../components/ozw/translations/cs.json | 2 + .../components/ozw/translations/zh-Hant.json | 9 ++- .../panasonic_viera/translations/zh-Hant.json | 2 +- .../plaato/translations/zh-Hant.json | 2 +- .../point/translations/zh-Hant.json | 2 +- .../poolsense/translations/zh-Hant.json | 2 +- .../powerwall/translations/zh-Hant.json | 2 +- .../profiler/translations/zh-Hant.json | 2 +- .../progettihwsw/translations/zh-Hant.json | 2 +- .../components/ps4/translations/zh-Hant.json | 10 +-- .../rachio/translations/zh-Hant.json | 4 +- .../rainmachine/translations/zh-Hant.json | 2 +- .../recollect_waste/translations/zh-Hant.json | 2 +- .../rfxtrx/translations/zh-Hant.json | 20 +++--- .../components/ring/translations/zh-Hant.json | 2 +- .../risco/translations/zh-Hant.json | 2 +- .../components/roku/translations/zh-Hant.json | 2 +- .../roomba/translations/zh-Hant.json | 2 +- .../components/roon/translations/zh-Hant.json | 2 +- .../rpi_power/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../samsungtv/translations/zh-Hant.json | 2 +- .../sense/translations/zh-Hant.json | 2 +- .../sentry/translations/zh-Hant.json | 2 +- .../shelly/translations/zh-Hant.json | 8 +-- .../components/smappee/translations/et.json | 2 +- .../smappee/translations/zh-Hant.json | 10 +-- .../translations/zh-Hant.json | 2 +- .../components/sms/translations/zh-Hant.json | 6 +- .../solaredge/translations/zh-Hant.json | 4 +- .../solarlog/translations/zh-Hant.json | 4 +- .../components/soma/translations/zh-Hant.json | 2 +- .../somfy/translations/zh-Hant.json | 2 +- .../songpal/translations/zh-Hant.json | 4 +- .../sonos/translations/zh-Hant.json | 4 +- .../speedtestdotnet/translations/zh-Hant.json | 2 +- .../spider/translations/zh-Hant.json | 2 +- .../spotify/translations/zh-Hans.json | 7 ++ .../squeezebox/translations/zh-Hant.json | 2 +- .../srp_energy/translations/zh-Hant.json | 2 +- .../syncthru/translations/zh-Hant.json | 4 +- .../synology_dsm/translations/zh-Hant.json | 2 +- .../components/tado/translations/zh-Hant.json | 2 +- .../tasmota/translations/zh-Hant.json | 2 +- .../components/toon/translations/zh-Hant.json | 2 +- .../tplink/translations/zh-Hant.json | 6 +- .../traccar/translations/zh-Hant.json | 2 +- .../tradfri/translations/zh-Hant.json | 2 +- .../transmission/translations/zh-Hant.json | 2 +- .../components/tuya/translations/et.json | 2 +- .../components/tuya/translations/zh-Hant.json | 26 ++++---- .../twilio/translations/zh-Hant.json | 2 +- .../twinkly/translations/zh-Hant.json | 4 +- .../unifi/translations/zh-Hant.json | 6 +- .../components/upb/translations/zh-Hant.json | 2 +- .../components/upnp/translations/zh-Hant.json | 8 +-- .../velbus/translations/zh-Hant.json | 4 +- .../components/vera/translations/et.json | 2 +- .../components/vera/translations/zh-Hant.json | 8 +-- .../vesync/translations/zh-Hant.json | 2 +- .../vilfo/translations/zh-Hant.json | 2 +- .../vizio/translations/zh-Hant.json | 14 ++-- .../volumio/translations/zh-Hant.json | 2 +- .../components/wemo/translations/zh-Hant.json | 4 +- .../wiffi/translations/zh-Hant.json | 2 +- .../wilight/translations/zh-Hant.json | 6 +- .../withings/translations/zh-Hant.json | 2 +- .../components/wled/translations/zh-Hant.json | 6 +- .../wolflink/translations/zh-Hant.json | 6 +- .../components/xbox/translations/zh-Hant.json | 2 +- .../xiaomi_aqara/translations/zh-Hant.json | 8 +-- .../xiaomi_miio/translations/zh-Hant.json | 6 +- .../yeelight/translations/zh-Hant.json | 8 +-- .../zerproc/translations/zh-Hant.json | 4 +- .../components/zha/translations/zh-Hant.json | 22 +++---- .../zwave/translations/zh-Hant.json | 6 +- 206 files changed, 739 insertions(+), 424 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/zh-Hans.json create mode 100644 homeassistant/components/apple_tv/translations/cs.json create mode 100644 homeassistant/components/apple_tv/translations/nl.json create mode 100644 homeassistant/components/apple_tv/translations/pl.json create mode 100644 homeassistant/components/apple_tv/translations/zh-Hant.json create mode 100644 homeassistant/components/hyperion/translations/nl.json create mode 100644 homeassistant/components/kulersky/translations/cs.json create mode 100644 homeassistant/components/kulersky/translations/et.json create mode 100644 homeassistant/components/kulersky/translations/zh-Hant.json create mode 100644 homeassistant/components/spotify/translations/zh-Hans.json diff --git a/homeassistant/components/abode/translations/nl.json b/homeassistant/components/abode/translations/nl.json index 9ef9c74aa1..a054d863c9 100644 --- a/homeassistant/components/abode/translations/nl.json +++ b/homeassistant/components/abode/translations/nl.json @@ -5,9 +5,15 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "invalid_mfa_code": "Ongeldige MFA-code" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-code (6-cijfers)" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json index d3e1db007f..6725df4445 100644 --- a/homeassistant/components/abode/translations/zh-Hant.json +++ b/homeassistant/components/abode/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index ebbceb69b0..bed28b6297 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "\u00dchendu Accuweatheri serveriga", + "can_reach_server": "\u00dchendus Accuweatheri serveriga", "remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud" } } diff --git a/homeassistant/components/accuweather/translations/zh-Hans.json b/homeassistant/components/accuweather/translations/zh-Hans.json new file mode 100644 index 0000000000..f8879f5715 --- /dev/null +++ b/homeassistant/components/accuweather/translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee AccuWeather \u670d\u52a1\u5668", + "remaining_requests": "\u5176\u4f59\u5141\u8bb8\u7684\u8bf7\u6c42" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index db6c097e1e..ed5fa26f0c 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -31,5 +31,11 @@ "title": "AccuWeather \u9078\u9805" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda AccuWeather \u4f3a\u670d\u5668", + "remaining_requests": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42" + } } } \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/zh-Hant.json b/homeassistant/components/acmeda/translations/zh-Hant.json index 1e7d4d0f14..2aeb94f66d 100644 --- a/homeassistant/components/acmeda/translations/zh-Hant.json +++ b/homeassistant/components/acmeda/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "step": { "user": { diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index b5c6863a94..8306b2daf7 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/advantage_air/translations/zh-Hant.json b/homeassistant/components/advantage_air/translations/zh-Hant.json index eae6626685..9d1cd4210f 100644 --- a/homeassistant/components/advantage_air/translations/zh-Hant.json +++ b/homeassistant/components/advantage_air/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant/components/agent_dvr/translations/zh-Hant.json index 16de4fd103..aa0ac965a8 100644 --- a/homeassistant/components/agent_dvr/translations/zh-Hant.json +++ b/homeassistant/components/agent_dvr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index 0d46a0f764..8cbfd13825 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -22,7 +22,7 @@ }, "system_health": { "info": { - "can_reach_server": "\u00dchendu Airly serveriga" + "can_reach_server": "\u00dchendus Airly serveriga" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/zh-Hans.json b/homeassistant/components/airly/translations/zh-Hans.json index f5b95a57f0..1a57bfbadf 100644 --- a/homeassistant/components/airly/translations/zh-Hans.json +++ b/homeassistant/components/airly/translations/zh-Hans.json @@ -10,5 +10,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index e8deb533de..4d60b158c4 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 913173f98c..4bdc295904 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -24,7 +24,7 @@ "ip_address": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u8a2d\u5099\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u8a2d\u5099 UI \u7372\u5f97\u3002", + "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u88dd\u7f6e\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u88dd\u7f6e UI \u7372\u5f97\u3002", "title": "\u8a2d\u5b9a AirVisual Node/Pro" }, "reauth_confirm": { diff --git a/homeassistant/components/alarmdecoder/translations/et.json b/homeassistant/components/alarmdecoder/translations/et.json index 12395b1d07..d09fb725e3 100644 --- a/homeassistant/components/alarmdecoder/translations/et.json +++ b/homeassistant/components/alarmdecoder/translations/et.json @@ -59,14 +59,14 @@ "zone_rfid": "RF jada\u00fchendus", "zone_type": "Ala t\u00fc\u00fcp" }, - "description": "Sisestage ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4tke ala nimi t\u00fchjaks.", + "description": "Sisesta ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4ta ala nimi t\u00fchjaks.", "title": "Seadista AlarmDecoder" }, "zone_select": { "data": { "zone_number": "Ala number" }, - "description": "Sisestage ala number mida soovite lisada, muuta v\u00f5i eemaldada.", + "description": "Sisesta ala number mida soovid lisada, muuta v\u00f5i eemaldada.", "title": "Seadista AlarmDecoder" } } diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index ee630bc7a1..a43e80d362 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "create_entry": { "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" @@ -12,8 +12,8 @@ "step": { "protocol": { "data": { - "device_baudrate": "\u8a2d\u5099\u901a\u8a0a\u7387", - "device_path": "\u8a2d\u5099\u8def\u5f91", + "device_baudrate": "\u88dd\u7f6e\u901a\u8a0a\u7387", + "device_path": "\u88dd\u7f6e\u8def\u5f91", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index a576b11e63..6312d4ecd1 100644 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ b/homeassistant/components/almond/translations/zh-Hant.json @@ -4,7 +4,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambient_station/translations/zh-Hant.json b/homeassistant/components/ambient_station/translations/zh-Hant.json index 51f0033b95..dab15def7b 100644 --- a/homeassistant/components/ambient_station/translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "invalid_key": "API \u5bc6\u9470\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" }, "step": { "user": { diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json new file mode 100644 index 0000000000..412d31082f --- /dev/null +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "pair_with_pin": { + "data": { + "pin": "PIN k\u00f3d" + } + }, + "user": { + "data": { + "device_input": "Za\u0159\u00edzen\u00ed" + } + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json new file mode 100644 index 0000000000..a11488ebca --- /dev/null +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.", + "device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.", + "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json new file mode 100644 index 0000000000..2fb8d234ea --- /dev/null +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "backoff": "Urz\u0105dzenie w tej chwili nie akceptuje \u017c\u0105da\u0144 parowania (by\u0107 mo\u017ce zbyt wiele razy wpisa\u0142e\u015b nieprawid\u0142owy kod PIN), spr\u00f3buj ponownie p\u00f3\u017aniej.", + "device_did_not_pair": "Nie podj\u0119to pr\u00f3by zako\u0144czenia procesu parowania z urz\u0105dzenia.", + "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index 5c62b1f53e..2fbdd53c07 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -1,25 +1,31 @@ { "config": { "abort": { - "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0435 \u0440\u0430\u0437.", - "no_devices_found": "\u041d\u0438\u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0441\u0435\u0442\u0438", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", - "invalid_auth": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", - "no_devices_found": "\u041d\u0438\u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0441\u0435\u0442\u0438", - "no_usable_service": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043d\u043e \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043d\u0435\u043c\u0443. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442\u0435 \u0432\u0438\u0434\u0435\u0442\u044c \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0435\u0433\u043e IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Apple TV.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Apple TV", + "flow_title": "Apple TV: {name}", "step": { "confirm": { "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 Apple TV" }, + "pair_no_pin": { + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u044b`{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u0435\u043c Apple TV.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + }, "pair_with_pin": { "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + "pin": "PIN-\u043a\u043e\u0434" } }, "reconfigure": { diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json new file mode 100644 index 0000000000..269e207e8a --- /dev/null +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", + "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", + "invalid_config": "\u6b64\u88dd\u7f6e\u8a2d\u5b9a\u4e0d\u5b8c\u6574\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u4e00\u6b21\u3002", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Apple TV\uff1a{name}", + "step": { + "confirm": { + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 Apple TV \u81f3 Home Assistant\u3002\n\n**\u6b32\u5b8c\u6210\u6b65\u9a5f\uff0c\u5fc5\u9808\u8f38\u5165\u591a\u7d44 PIN \u78bc\u3002**\n\n\u8acb\u6ce8\u610f\uff1a\u6b64\u6574\u5408\u4e26 *\u7121\u6cd5* \u9032\u884c Apple TV \u95dc\u6a5f\u7684\u52d5\u4f5c\uff0c\u50c5\u80fd\u65bc Home Assistant \u4e2d\u95dc\u9589\u5a92\u9ad4\u64ad\u653e\u5668\u529f\u80fd\uff01", + "title": "\u78ba\u8a8d\u65b0\u589e Apple TV" + }, + "pair_no_pin": { + "description": "`{protocol}` \u670d\u52d9\u9700\u8981\u9032\u884c\u914d\u5c0d\uff0c\u8acb\u8f38\u5165 Apple TV \u4e0a\u6240\u986f\u793a\u4e4b PIN {pin} \u4ee5\u7e7c\u7e8c\u3002", + "title": "\u914d\u5c0d\u4e2d" + }, + "pair_with_pin": { + "data": { + "pin": "PIN \u78bc" + }, + "description": "\u914d\u5c0d\u9700\u8981 `{protocol}` \u901a\u8a0a\u5354\u5b9a\u3002\u8acb\u8f38\u5165\u986f\u793a\u65bc\u756b\u9762\u4e0a\u7684 PIN \u78bc\uff0c\u524d\u65b9\u7684 0 \u53ef\u5ffd\u8996\u986f\u793a\u78bc\u70ba 0123\uff0c\u5247\u8f38\u5165 123\u3002", + "title": "\u914d\u5c0d\u4e2d" + }, + "reconfigure": { + "description": "\u6b64 Apple TV \u906d\u9047\u5230\u4e00\u4e9b\u9023\u7dda\u554f\u984c\uff0c\u5fc5\u9808\u91cd\u65b0\u8a2d\u5b9a\u3002", + "title": "\u88dd\u7f6e\u91cd\u65b0\u8a2d\u5b9a" + }, + "service_problem": { + "description": "\u7576\u914d\u5c0d `{protocol}` \u6642\u767c\u751f\u554f\u984c\uff0c\u5c07\u6703\u9032\u884c\u5ffd\u7565\u3002", + "title": "\u65b0\u589e\u670d\u52d9\u5931\u6557" + }, + "user": { + "data": { + "device_input": "\u88dd\u7f6e" + }, + "description": "\u9996\u5148\u8f38\u5165\u6240\u8981\u65b0\u589e\u7684 Apple TV \u88dd\u7f6e\u540d\u7a31\uff08\u4f8b\u5982\u5eda\u623f\u6216\u81e5\u5ba4\uff09\u6216 IP \u4f4d\u5740\u3002\u5047\u5982\u65bc\u5340\u7db2\u4e0a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\uff0c\u5c07\u6703\u986f\u793a\u65bc\u4e0b\u65b9\u3002\n\n\u5047\u5982\u7121\u6cd5\u770b\u5230\u88dd\u7f6e\u6216\u906d\u9047\u4efb\u4f55\u554f\u984c\uff0c\u8acb\u8a66\u8457\u6307\u5b9a\u88dd\u7f6e\u7684 IP \u4f4d\u5740\u3002\n\n{devices}", + "title": "\u8a2d\u5b9a\u4e00\u7d44 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u7576\u958b\u59cb Home Assistant \u6642\u4e0d\u8981\u958b\u555f\u88dd\u7f6e" + }, + "description": "\u8a2d\u5b9a\u4e00\u822c\u88dd\u7f6e\u8a2d\u5b9a" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json index fd2cb2181a..853b498a51 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, @@ -15,7 +15,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u7aef\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\u3002" + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u7aef\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\u3002" } } }, diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index 164e87a964..b616437aa2 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a8d\u8b49\u8acb\u6c42" + "unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a8d\u8b49\u8acb\u6c42" }, "step": { "user": { @@ -14,7 +14,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } } diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index 8b40a8edef..11fe9ff88b 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index 07cc81cc0f..1d7aaa7c74 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "Axis \u8a2d\u5099\uff1a{name} ({host})", + "flow_title": "Axis \u88dd\u7f6e\uff1a{name} ({host})", "step": { "user": { "data": { @@ -20,7 +20,7 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "title": "\u8a2d\u5b9a Axis \u8a2d\u5099" + "title": "\u8a2d\u5b9a Axis \u88dd\u7f6e" } } }, @@ -30,7 +30,7 @@ "data": { "stream_profile": "\u9078\u64c7\u6240\u8981\u4f7f\u7528\u7684\u4e32\u6d41\u8a2d\u5b9a" }, - "title": "Axis \u8a2d\u5099\u5f71\u50cf\u4e32\u6d41\u9078\u9805" + "title": "Axis \u88dd\u7f6e\u5f71\u50cf\u4e32\u6d41\u9078\u9805" } } } diff --git a/homeassistant/components/blebox/translations/zh-Hant.json b/homeassistant/components/blebox/translations/zh-Hant.json index 5d11c2e9a7..b84105745a 100644 --- a/homeassistant/components/blebox/translations/zh-Hant.json +++ b/homeassistant/components/blebox/translations/zh-Hant.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "unsupported_version": "BleBox \u8a2d\u5099\u97cc\u9ad4\u904e\u820a\uff0c\u8acb\u5148\u9032\u884c\u66f4\u65b0\u3002" + "unsupported_version": "BleBox \u88dd\u7f6e\u97cc\u9ad4\u904e\u820a\uff0c\u8acb\u5148\u9032\u884c\u66f4\u65b0\u3002" }, - "flow_title": "BleBox \u8a2d\u5099\uff1a{name} ({host})", + "flow_title": "BleBox \u88dd\u7f6e\uff1a{name} ({host})", "step": { "user": { "data": { @@ -17,7 +17,7 @@ "port": "\u901a\u8a0a\u57e0" }, "description": "\u8a2d\u5b9a BleBox \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", - "title": "\u8a2d\u5b9a BleBox \u8a2d\u5099" + "title": "\u8a2d\u5b9a BleBox \u88dd\u7f6e" } } } diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index 5736c91714..3d05dc82ab 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index cbb42aee92..af652c5450 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "old_firmware": "Bond \u8a2d\u5099\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c", + "old_firmware": "Bond \u88dd\u7f6e\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "Bond\uff1a{bond_id} ({host})", diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index eafe98f154..53dc9ead65 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002" }, "error": { diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json index 8781b90c3d..2e0864c9f7 100644 --- a/homeassistant/components/broadlink/translations/zh-Hant.json +++ b/homeassistant/components/broadlink/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", - "not_supported": "\u8a2d\u5099\u4e0d\u652f\u63f4", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -16,31 +16,31 @@ "flow_title": "{name}\uff08\u4f4d\u65bc {host} \u4e4b {model} \uff09", "step": { "auth": { - "title": "\u8a8d\u8b49\u8a2d\u5099" + "title": "\u8a8d\u8b49\u88dd\u7f6e" }, "finish": { "data": { "name": "\u540d\u7a31" }, - "title": "\u9078\u64c7\u8a2d\u5099\u540d\u7a31" + "title": "\u9078\u64c7\u88dd\u7f6e\u540d\u7a31" }, "reset": { - "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u8a2d\u5099\u5df2\u9396\u5b9a\uff0c\u9700\u8981\u89e3\u9396\u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u8207\u5b8c\u6210\u8a2d\u5b9a\uff0c\u8acb\u8ddf\u96a8\u6307\u793a\uff1a\n1. \u958b\u555f Broadlink App\u3002\n2. \u9ede\u9078\u8a2d\u5099\u3002\n3. \u9ede\u9078\u53f3\u4e0a\u65b9\u7684 `...`\u3002\n4. \u6372\u52d5\u81f3\u6700\u5e95\u9801\u3002\n5. \u95dc\u9589\u9396\u5b9a\u3002", - "title": "\u89e3\u9396\u8a2d\u5099" + "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u88dd\u7f6e\u5df2\u9396\u5b9a\uff0c\u9700\u8981\u89e3\u9396\u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u8207\u5b8c\u6210\u8a2d\u5b9a\uff0c\u8acb\u8ddf\u96a8\u6307\u793a\uff1a\n1. \u958b\u555f Broadlink App\u3002\n2. \u9ede\u9078\u88dd\u7f6e\u3002\n3. \u9ede\u9078\u53f3\u4e0a\u65b9\u7684 `...`\u3002\n4. \u6372\u52d5\u81f3\u6700\u5e95\u9801\u3002\n5. \u95dc\u9589\u9396\u5b9a\u3002", + "title": "\u89e3\u9396\u88dd\u7f6e" }, "unlock": { "data": { "unlock": "\u662f\uff0c\u57f7\u884c\u3002" }, - "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u8a2d\u5099\u5df2\u9396\u5b9a\uff0c\u53ef\u80fd\u5c0e\u81f4 Home Assistant \u8a8d\u8b49\u554f\u984c\uff0c\u662f\u5426\u8981\u89e3\u9396\uff1f", - "title": "\u89e3\u9396\u8a2d\u5099\uff08\u9078\u9805\uff09" + "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u88dd\u7f6e\u5df2\u9396\u5b9a\uff0c\u53ef\u80fd\u5c0e\u81f4 Home Assistant \u8a8d\u8b49\u554f\u984c\uff0c\u662f\u5426\u8981\u89e3\u9396\uff1f", + "title": "\u89e3\u9396\u88dd\u7f6e\uff08\u9078\u9805\uff09" }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", "timeout": "\u903e\u6642" }, - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } } diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 79dc4c81b2..d8208e6ce4 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" }, "error": { diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json index 7ada76c1d2..3fefe08f98 100644 --- a/homeassistant/components/bsblan/translations/zh-Hant.json +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -16,8 +16,8 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8a2d\u5b9a BSB-Lan \u8a2d\u5099\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", - "title": "\u9023\u7dda\u81f3 BSB-Lan \u8a2d\u5099" + "description": "\u8a2d\u5b9a BSB-Lan \u88dd\u7f6e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7dda\u81f3 BSB-Lan \u88dd\u7f6e" } } } diff --git a/homeassistant/components/canary/translations/zh-Hant.json b/homeassistant/components/canary/translations/zh-Hant.json index 07463bc8a1..c53ffd8327 100644 --- a/homeassistant/components/canary/translations/zh-Hant.json +++ b/homeassistant/components/canary/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 91a0dc60be..90c98e491d 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/cloud/translations/et.json b/homeassistant/components/cloud/translations/et.json index 07e7748e13..19f8f40b9d 100644 --- a/homeassistant/components/cloud/translations/et.json +++ b/homeassistant/components/cloud/translations/et.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa on lubatud", - "can_reach_cert_server": "\u00dchendu serdiserveriga", - "can_reach_cloud": "\u00dchendu Home Assistant Cloudiga", - "can_reach_cloud_auth": "\u00dchendu tuvastusserveriga", + "can_reach_cert_server": "\u00dchendus serdiserveriga", + "can_reach_cloud": "\u00dchendus Home Assistant Cloudiga", + "can_reach_cloud_auth": "\u00dchendus tuvastusserveriga", "google_enabled": "Google on lubatud", "logged_in": "Sisse logitud", "relayer_connected": "Edastaja on \u00fchendatud", diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json index e84966b8d5..1be70def03 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hant.json +++ b/homeassistant/components/cloudflare/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/control4/translations/zh-Hant.json b/homeassistant/components/control4/translations/zh-Hant.json index f52e877a9d..bc955f119e 100644 --- a/homeassistant/components/control4/translations/zh-Hant.json +++ b/homeassistant/components/control4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/coolmaster/translations/zh-Hant.json b/homeassistant/components/coolmaster/translations/zh-Hant.json index 03f9cb3cfb..42278561d5 100644 --- a/homeassistant/components/coolmaster/translations/zh-Hant.json +++ b/homeassistant/components/coolmaster/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u8a2d\u5099\u3002" + "no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index 1949bd98b2..b1a19792a0 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -16,7 +16,7 @@ "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u8a2d\u5099\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", + "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u88dd\u7f6e\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", "title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf" } } diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index f6695fc2af..335aa73a67 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -4,9 +4,9 @@ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u8a2d\u5099\u9023\u7dda", - "not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099", - "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u8a2d\u5099" + "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u88dd\u7f6e\u9023\u7dda", + "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", + "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u88dd\u7f6e" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" @@ -59,7 +59,7 @@ "turn_on": "\u958b\u555f" }, "trigger_type": { - "remote_awakened": "\u8a2d\u5099\u5df2\u559a\u9192", + "remote_awakened": "\u88dd\u7f6e\u5df2\u559a\u9192", "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", @@ -71,22 +71,22 @@ "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", - "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", - "remote_double_tap_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u96d9\u9ede\u9078", - "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", - "remote_flip_180_degrees": "\u8a2d\u5099\u65cb\u8f49 180 \u5ea6", - "remote_flip_90_degrees": "\u8a2d\u5099\u65cb\u8f49 90 \u5ea6", - "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", - "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", - "remote_moved_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u671d\u4e0a", - "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_turned_clockwise": "\u8a2d\u5099\u9806\u6642\u91dd\u65cb\u8f49", - "remote_turned_counter_clockwise": "\u8a2d\u5099\u9006\u6642\u91dd\u65cb\u8f49" + "remote_double_tap": "\u88dd\u7f6e \"{subtype}\" \u96d9\u6572", + "remote_double_tap_any_side": "\u88dd\u7f6e\u4efb\u4e00\u9762\u96d9\u9ede\u9078", + "remote_falling": "\u88dd\u7f6e\u81ea\u7531\u843d\u4e0b", + "remote_flip_180_degrees": "\u88dd\u7f6e\u65cb\u8f49 180 \u5ea6", + "remote_flip_90_degrees": "\u88dd\u7f6e\u65cb\u8f49 90 \u5ea6", + "remote_gyro_activated": "\u88dd\u7f6e\u6416\u6643", + "remote_moved": "\u88dd\u7f6e\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_moved_any_side": "\u88dd\u7f6e\u4efb\u4e00\u9762\u671d\u4e0a", + "remote_rotate_from_side_1": "\u88dd\u7f6e\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_2": "\u88dd\u7f6e\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_3": "\u88dd\u7f6e\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_4": "\u88dd\u7f6e\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_5": "\u88dd\u7f6e\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_6": "\u88dd\u7f6e\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_turned_clockwise": "\u88dd\u7f6e\u9806\u6642\u91dd\u65cb\u8f49", + "remote_turned_counter_clockwise": "\u88dd\u7f6e\u9006\u6642\u91dd\u65cb\u8f49" } }, "options": { @@ -95,9 +95,9 @@ "data": { "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44", - "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u8a2d\u5099" + "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u88dd\u7f6e" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b", + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b", "title": "deCONZ \u9078\u9805" } } diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index fab16780a5..1aaa5b0407 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", diff --git a/homeassistant/components/device_tracker/translations/zh-Hant.json b/homeassistant/components/device_tracker/translations/zh-Hant.json index e80c32afd0..b0e44bedac 100644 --- a/homeassistant/components/device_tracker/translations/zh-Hant.json +++ b/homeassistant/components/device_tracker/translations/zh-Hant.json @@ -15,5 +15,5 @@ "not_home": "\u96e2\u5bb6" } }, - "title": "\u8a2d\u5099\u8ffd\u8e64\u5668" + "title": "\u88dd\u7f6e\u8ffd\u8e64\u5668" } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json index f0b6c3eade..989db1c256 100644 --- a/homeassistant/components/dialogflow/translations/et.json +++ b/homeassistant/components/dialogflow/translations/et.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, "create_entry": { - "default": "S\u00fcndmuste saatmiseks Home Assistantile peate seadistama [Dialogflow'i veebihaagii integreerimine] ( {dialogflow_url} ). \n\n Sisestage j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n - Sisu t\u00fc\u00fcp: rakendus / json \n\n Lisateavet leiate [dokumentatsioonist] ( {docs_url} )." + "default": "S\u00fcndmuste saatmiseks Home Assistantile peate seadistama [Dialogflow'i veebihaagii integreerimine] ( {dialogflow_url} ). \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n - Sisu t\u00fc\u00fcp: rakendus / json \n\n Lisateavet leiate [dokumentatsioonist] ( {docs_url} )." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/translations/zh-Hant.json b/homeassistant/components/dialogflow/translations/zh-Hant.json index ab790dafe9..4584a38313 100644 --- a/homeassistant/components/dialogflow/translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index 9be7ac31e6..e19ff18b36 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index a4b3bd2fd8..bb1d109bb8 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_doorbird_device": "\u6b64\u8a2d\u5099\u4e26\u975e DoorBird" + "not_doorbird_device": "\u6b64\u88dd\u7f6e\u4e26\u975e DoorBird" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -15,7 +15,7 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "\u8a2d\u5099\u540d\u7a31", + "name": "\u88dd\u7f6e\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index e35c96a7bf..cbbc3dc8f5 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" } }, "options": { diff --git a/homeassistant/components/dunehd/translations/zh-Hant.json b/homeassistant/components/dunehd/translations/zh-Hant.json index 855cefaa77..ce7a120122 100644 --- a/homeassistant/components/dunehd/translations/zh-Hant.json +++ b/homeassistant/components/dunehd/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" }, diff --git a/homeassistant/components/eafm/translations/zh-Hant.json b/homeassistant/components/eafm/translations/zh-Hant.json index 5da4b6d7c0..73083d2b73 100644 --- a/homeassistant/components/eafm/translations/zh-Hant.json +++ b/homeassistant/components/eafm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_stations": "\u627e\u4e0d\u5230\u7b26\u5408\u7684\u76e3\u63a7\u7ad9\u3002" }, "step": { diff --git a/homeassistant/components/ecobee/translations/zh-Hant.json b/homeassistant/components/ecobee/translations/zh-Hant.json index 54cad2049f..e9789c855d 100644 --- a/homeassistant/components/ecobee/translations/zh-Hant.json +++ b/homeassistant/components/ecobee/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index e25b4cd7c8..8f301b73b3 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -17,8 +17,8 @@ "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato Key \u7167\u660e\u8a2d\u5099\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato Key \u7167\u660e\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u88dd\u7f6e" } } } diff --git a/homeassistant/components/emulated_roku/translations/zh-Hant.json b/homeassistant/components/emulated_roku/translations/zh-Hant.json index 8c4ac5a0d7..ee877f7896 100644 --- a/homeassistant/components/emulated_roku/translations/zh-Hant.json +++ b/homeassistant/components/emulated_roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "step": { "user": { diff --git a/homeassistant/components/enocean/translations/zh-Hant.json b/homeassistant/components/enocean/translations/zh-Hant.json index bc51c7f0bb..6000b968e5 100644 --- a/homeassistant/components/enocean/translations/zh-Hant.json +++ b/homeassistant/components/enocean/translations/zh-Hant.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "invalid_dongle_path": "\u8a2d\u5099\u8def\u5f91\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "invalid_dongle_path": "\u88dd\u7f6e\u8def\u5f91\u7121\u6548", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "invalid_dongle_path": "\u6b64\u8def\u5f91\u7121\u6709\u6548\u8a2d\u5099" + "invalid_dongle_path": "\u6b64\u8def\u5f91\u7121\u6709\u6548\u88dd\u7f6e" }, "step": { "detect": { "data": { - "path": "USB \u8a2d\u5099\u8def\u5f91" + "path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "title": "\u9078\u64c7 ENOcean \u8a2d\u5099\u8def\u5f91" + "title": "\u9078\u64c7 ENOcean \u88dd\u7f6e\u8def\u5f91" }, "manual": { "data": { - "path": "USB \u8a2d\u5099\u8def\u5f91" + "path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "title": "\u8f38\u5165 ENOcean \u8a2d\u5099\u8def\u5f91" + "title": "\u8f38\u5165 ENOcean \u88dd\u7f6e\u8def\u5f91" } } } diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index a60ef4fdeb..4e719a7957 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/flo/translations/zh-Hant.json b/homeassistant/components/flo/translations/zh-Hant.json index 8bf65ef6ee..cad7d736a9 100644 --- a/homeassistant/components/flo/translations/zh-Hant.json +++ b/homeassistant/components/flo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index 88e8628848..0ac0bac013 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_forked_daapd": "\u8a2d\u5099\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_forked_daapd": "\u88dd\u7f6e\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" }, "error": { "forbidden": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d forked-daapd \u7db2\u8def\u6b0a\u9650\u3002", @@ -21,7 +21,7 @@ "password": "API \u5bc6\u78bc\uff08\u5047\u5982\u7121\u5bc6\u78bc\uff0c\u8acb\u7559\u7a7a\uff09", "port": "API \u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a forked-daapd \u8a2d\u5099" + "title": "\u8a2d\u5b9a forked-daapd \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json index 608c5bbcba..734498585f 100644 --- a/homeassistant/components/freebox/translations/zh-Hant.json +++ b/homeassistant/components/freebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index b09eac7761..7b85df577e 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/geofency/translations/zh-Hant.json b/homeassistant/components/geofency/translations/zh-Hant.json index 0bef632c5a..4ffe673045 100644 --- a/homeassistant/components/geofency/translations/zh-Hant.json +++ b/homeassistant/components/geofency/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/glances/translations/zh-Hant.json b/homeassistant/components/glances/translations/zh-Hant.json index 0054edbdb0..d81ca02f6b 100644 --- a/homeassistant/components/glances/translations/zh-Hant.json +++ b/homeassistant/components/glances/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json index b033c16afb..5c25a8cb98 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -14,7 +14,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u60a8\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u63a5\u8005\u7531\u8def\u7531\u5668\u53d6\u5f97\u4e3b\u6a5f\u7aef IP\uff0c \u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u8a2d\u5099\u7684 DHCP \u4ee5\u78ba\u4fdd\u4e3b\u6a5f\u7aef IP \u4e0d\u81f3\u65bc\u6539\u8b8a\u3002\u8acb\u53c3\u8003\u60a8\u7684\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u64cd\u4f5c\u3002", + "description": "\u60a8\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u63a5\u8005\u7531\u8def\u7531\u5668\u53d6\u5f97\u4e3b\u6a5f\u7aef IP\uff0c \u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u88dd\u7f6e\u7684 DHCP \u4ee5\u78ba\u4fdd\u4e3b\u6a5f\u7aef IP \u4e0d\u81f3\u65bc\u6539\u8b8a\u3002\u8acb\u53c3\u8003\u60a8\u7684\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u64cd\u4f5c\u3002", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gpslogger/translations/zh-Hant.json b/homeassistant/components/gpslogger/translations/zh-Hant.json index 324a92cd8b..9c5448266e 100644 --- a/homeassistant/components/gpslogger/translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/gree/translations/zh-Hant.json b/homeassistant/components/gree/translations/zh-Hant.json index 91a0dc60be..90c98e491d 100644 --- a/homeassistant/components/gree/translations/zh-Hant.json +++ b/homeassistant/components/gree/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index e40cef4f94..bf3a1606e6 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, @@ -11,10 +11,10 @@ "ip_address": "IP \u4f4d\u5740", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u8a2d\u5099\u3002" + "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u88dd\u7f6e\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u8a2d\u5099\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u88dd\u7f6e\uff1f" } } } diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index 4ab79dd603..608a2150c6 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json index 95b4a8a8a6..0d74360b8f 100644 --- a/homeassistant/components/hassio/translations/zh-Hans.json +++ b/homeassistant/components/hassio/translations/zh-Hans.json @@ -12,7 +12,7 @@ "supervisor_version": "Supervisor \u7248\u672c", "supported": "\u53d7\u652f\u6301", "update_channel": "\u66f4\u65b0\u901a\u9053", - "version_api": "API\u7248\u672c" + "version_api": "API \u7248\u672c" } }, "title": "Hass.io" diff --git a/homeassistant/components/heos/translations/zh-Hant.json b/homeassistant/components/heos/translations/zh-Hant.json index 95ddf7e51a..fe3e8fb7b4 100644 --- a/homeassistant/components/heos/translations/zh-Hant.json +++ b/homeassistant/components/heos/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -11,7 +11,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", "title": "\u9023\u7dda\u81f3 Heos" } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json index 56ad5128d9..e08a2c5f6d 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json +++ b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/hlk_sw16/translations/zh-Hant.json b/homeassistant/components/hlk_sw16/translations/zh-Hant.json index 8bf65ef6ee..cad7d736a9 100644 --- a/homeassistant/components/hlk_sw16/translations/zh-Hant.json +++ b/homeassistant/components/hlk_sw16/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index e3e9247e7c..0f1093f5b5 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -48,7 +48,7 @@ "include_domains": "\u5305\u542b Domain", "mode": "\u6a21\u5f0f" }, - "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u8a2d\u5099\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b Domains\"\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", + "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u88dd\u7f6e\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b Domains\"\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", "title": "\u9078\u64c7\u6240\u8981\u63a5\u901a\u7684 Domain\u3002" }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index 75c46125c1..6490904a32 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -1,49 +1,49 @@ { "config": { "abort": { - "accessory_not_found_error": "\u627e\u4e0d\u5230\u8a2d\u5099\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", + "accessory_not_found_error": "\u627e\u4e0d\u5230\u88dd\u7f6e\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", "already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "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", + "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u88dd\u7f6e\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "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": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u5be6\u9ad4\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", - "invalid_properties": "\u8a2d\u5099\u5ba3\u544a\u5c6c\u6027\u7121\u6548\u3002", - "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" + "invalid_config_entry": "\u6b64\u88dd\u7f6e\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u5be6\u9ad4\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "invalid_properties": "\u88dd\u7f6e\u5ba3\u544a\u5c6c\u6027\u7121\u6548\u3002", + "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u88dd\u7f6e" }, "error": { "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", - "max_peers_error": "\u8a2d\u5099\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "pairing_failed": "\u7576\u8a66\u5716\u8207\u8a2d\u5099\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u8a2d\u5099\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\u3002", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", - "unknown_error": "\u8a2d\u5099\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" + "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, "flow_title": "{name} \u4f7f\u7528 HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a", "step": { "busy_error": { - "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u8a2d\u5099\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", - "title": "\u8a2d\u5099\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d" + "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u88dd\u7f6e\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "title": "\u88dd\u7f6e\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d" }, "max_tries_error": { - "description": "\u8a2d\u5099\u5df2\u8d85\u904e 100 \u6b21\u8a8d\u8b49\u5617\u8a66\u6b21\u6578\uff0c\u8acb\u5617\u8a66\u91cd\u65b0\u555f\u52d5\u8a2d\u5099\u3001\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "description": "\u88dd\u7f6e\u5df2\u8d85\u904e 100 \u6b21\u8a8d\u8b49\u5617\u8a66\u6b21\u6578\uff0c\u8acb\u5617\u8a66\u91cd\u65b0\u555f\u52d5\u88dd\u7f6e\u3001\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", "title": "\u5df2\u8d85\u904e\u6700\u5927\u9a57\u8b49\u5617\u8a66\u6b21\u6578" }, "pair": { "data": { "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u8a2d\u5099\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", - "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u8a2d\u5099" + "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", + "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u88dd\u7f6e" }, "protocol_error": { - "description": "\u8a2d\u5099\u4e26\u672a\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff0c\u53ef\u80fd\u9700\u8981\u6309\u4e0b\u5be6\u9ad4\u6216\u865b\u64ec\u6309\u9215\u3002\u8acb\u78ba\u5b9a\u8a2d\u5099\u5df2\u7d93\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u3001\u6216\u91cd\u555f\u8a2d\u5099\uff0c\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "description": "\u88dd\u7f6e\u4e26\u672a\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff0c\u53ef\u80fd\u9700\u8981\u6309\u4e0b\u5be6\u9ad4\u6216\u865b\u64ec\u6309\u9215\u3002\u8acb\u78ba\u5b9a\u88dd\u7f6e\u5df2\u7d93\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u3001\u6216\u91cd\u555f\u88dd\u7f6e\uff0c\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", "title": "\u8207\u914d\u4ef6\u901a\u8a0a\u932f\u8aa4" }, "user": { "data": { - "device": "\u8a2d\u5099" + "device": "\u88dd\u7f6e" }, - "description": "\u4f7f\u7528\u5340\u57df\u7db2\u8def\u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u9078\u64c7\u6240\u8981\u65b0\u589e\u914d\u5c0d\u7684\u8a2d\u5099\uff1a", - "title": "\u8a2d\u5099\u9078\u64c7" + "description": "\u4f7f\u7528\u5340\u57df\u7db2\u8def\u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u9078\u64c7\u6240\u8981\u65b0\u589e\u914d\u5c0d\u7684\u88dd\u7f6e\uff1a", + "title": "\u88dd\u7f6e\u9078\u64c7" } } }, diff --git a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json index 21c896182f..066ce89c2b 100644 --- a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "connection_aborted": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "Access point ID (SGTIN)", - "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u8a2d\u5099\u7684\u5b57\u9996\u7528\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", "pin": "PIN \u78bc" }, "title": "\u9078\u64c7 HomematicIP Access point" diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index f0ea5d28ff..c8b067c887 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u8a2d\u5099" + "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e" }, "error": { "connection_timeout": "\u9023\u7dda\u903e\u6642", @@ -12,7 +12,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_url": "\u7db2\u5740\u7121\u6548", "login_attempts_exceeded": "\u5df2\u9054\u5617\u8a66\u767b\u5165\u6700\u5927\u6b21\u6578\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66", - "response_error": "\u4f86\u81ea\u8a2d\u5099\u672a\u77e5\u932f\u8aa4", + "response_error": "\u4f86\u81ea\u88dd\u7f6e\u672a\u77e5\u932f\u8aa4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "\u83ef\u70ba LTE\uff1a{name}", @@ -23,7 +23,7 @@ "url": "\u7db2\u5740", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u8a2d\u5099\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u8a2d\u5099 Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", + "description": "\u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u88dd\u7f6e Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", "title": "\u8a2d\u5b9a\u83ef\u70ba LTE" } } @@ -34,7 +34,7 @@ "data": { "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", - "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" + "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e" } } } diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index 9a5c0b2b54..ffb2b3a0e5 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -2,12 +2,12 @@ "config": { "abort": { "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", - "not_hue_bridge": "\u975e Hue Bridge \u8a2d\u5099", + "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json index 85d167fb04..e78e05855c 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hvv_departures/translations/zh-Hant.json b/homeassistant/components/hvv_departures/translations/zh-Hant.json index a965fa3881..df1eb910d2 100644 --- a/homeassistant/components/hvv_departures/translations/zh-Hant.json +++ b/homeassistant/components/hvv_departures/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json new file mode 100644 index 0000000000..d93018f8a3 --- /dev/null +++ b/homeassistant/components/hyperion/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "create_token": "Maak automatisch een nieuw token aan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/zh-Hant.json b/homeassistant/components/iaqualink/translations/zh-Hant.json index 3923f95f71..aaf1800f74 100644 --- a/homeassistant/components/iaqualink/translations/zh-Hant.json +++ b/homeassistant/components/iaqualink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json index 3a6f1b64fa..1c16db77fa 100644 --- a/homeassistant/components/icloud/translations/zh-Hant.json +++ b/homeassistant/components/icloud/translations/zh-Hant.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_device": "\u8a2d\u5099\u7686\u672a\u958b\u555f\u300c\u5c0b\u627e\u6211\u7684 iPhone\u300d\u529f\u80fd\u3002", + "no_device": "\u88dd\u7f6e\u7686\u672a\u958b\u555f\u300c\u5c0b\u627e\u6211\u7684 iPhone\u300d\u529f\u80fd\u3002", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557", - "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u8a2d\u5099\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" + "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u88dd\u7f6e\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" }, "step": { "reauth": { @@ -20,10 +20,10 @@ }, "trusted_device": { "data": { - "trusted_device": "\u4fe1\u4efb\u8a2d\u5099" + "trusted_device": "\u4fe1\u4efb\u88dd\u7f6e" }, - "description": "\u9078\u64c7\u4fe1\u4efb\u8a2d\u5099", - "title": "iCloud \u4fe1\u4efb\u8a2d\u5099" + "description": "\u9078\u64c7\u4fe1\u4efb\u88dd\u7f6e", + "title": "iCloud \u4fe1\u4efb\u88dd\u7f6e" }, "user": { "data": { diff --git a/homeassistant/components/ifttt/translations/et.json b/homeassistant/components/ifttt/translations/et.json index cecd2aea7e..e4d2c8f148 100644 --- a/homeassistant/components/ifttt/translations/et.json +++ b/homeassistant/components/ifttt/translations/et.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, "create_entry": { - "default": "S\u00fcndmuste saatmiseks Home Assistantile peate kasutama toimingut \"Make a web request\" [IFTTT Webhooki apletilt] ({applet_url}).\n\nSisestage j\u00e4rgmine teave:\n\n- URL: {webhook_url}.\n- Method: POST\n- Content Type: application/json\n\nVaadake [dokumentatsiooni]({docs_url}) kuidas seadistada sissetulevate andmete t\u00f6\u00f6tlemiseks automatiseerimisi." + "default": "S\u00fcndmuste saatmiseks Home Assistantile peate kasutama toimingut \"Make a web request\" [IFTTT Webhooki apletilt] ({applet_url}).\n\nSisesta j\u00e4rgmine teave:\n\n- URL: {webhook_url}.\n- Method: POST\n- Content Type: application/json\n\nVaadake [dokumentatsiooni]({docs_url}) kuidas seadistada sissetulevate andmete t\u00f6\u00f6tlemiseks automatiseerimisi." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/translations/zh-Hant.json b/homeassistant/components/ifttt/translations/zh-Hant.json index beef1c7070..fe5b80f72f 100644 --- a/homeassistant/components/ifttt/translations/zh-Hant.json +++ b/homeassistant/components/ifttt/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 923fdbfb44..d2f73fca37 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -5,14 +5,17 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "select_single": "Selecteer een optie." }, "step": { "hubv1": { "data": { "host": "IP-adres", "port": "Poort" - } + }, + "description": "Configureer de Insteon Hub versie 1 (pre-2014).", + "title": "Insteon Hub versie 1" }, "hubv2": { "data": { @@ -20,7 +23,13 @@ "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" - } + }, + "description": "Configureer de Insteon Hub versie 2.", + "title": "Insteon Hub versie 2" + }, + "plm": { + "description": "Configureer de Insteon PowerLink Modem (PLM).", + "title": "Insteon PLM" }, "user": { "data": { @@ -33,11 +42,29 @@ }, "options": { "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "input_error": "Ongeldige invoer, controleer uw waarden.", + "select_single": "Selecteer \u00e9\u00e9n optie." }, "step": { + "add_override": { + "data": { + "address": "Apparaatadres (bijv. 1a2b3c)", + "cat": "Apparaatcategorie (bijv. 0x10)", + "subcat": "Apparaatsubcategorie (bijv. 0x0a)" + }, + "description": "Voeg een apparaat overschrijven toe.", + "title": "Insteon" + }, "add_x10": { - "description": "Wijzig het wachtwoord van de Insteon Hub." + "data": { + "housecode": "Huiscode (a - p)", + "platform": "Platform", + "steps": "Dimmerstappen (alleen voor verlichtingsapparaten, standaard 22)", + "unitcode": "Unitcode (1 - 16)" + }, + "description": "Wijzig het wachtwoord van de Insteon Hub.", + "title": "Insteon" }, "change_hub_config": { "data": { @@ -46,7 +73,17 @@ "port": "Poort", "username": "Gebruikersnaam" }, - "description": "Wijzig de verbindingsgegevens van de Insteon Hub. Je moet Home Assistant opnieuw opstarten nadat je deze wijziging hebt aangebracht. Dit verandert niets aan de configuratie van de Hub zelf. Gebruik de Hub-app om de configuratie in de Hub te wijzigen." + "description": "Wijzig de verbindingsgegevens van de Insteon Hub. Je moet Home Assistant opnieuw opstarten nadat je deze wijziging hebt aangebracht. Dit verandert niets aan de configuratie van de Hub zelf. Gebruik de Hub-app om de configuratie in de Hub te wijzigen.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Voeg een apparaat overschrijven toe.", + "add_x10": "Voeg een X10-apparaat toe.", + "change_hub_config": "Wijzig de Hub-configuratie.", + "remove_override": "Verwijder een apparaatoverschrijving.", + "remove_x10": "Verwijder een X10-apparaat." + } } } } diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index 5358cccb85..dd69e0ec7c 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -29,7 +29,7 @@ }, "plm": { "data": { - "device": "USB \u8a2d\u5099\u8def\u5f91" + "device": "USB \u88dd\u7f6e\u8def\u5f91" }, "description": "\u8a2d\u5b9a PowerLink Modem (PLM)\u3002", "title": "Insteon PLM" @@ -52,18 +52,18 @@ "step": { "add_override": { "data": { - "address": "\u8a2d\u5099\u4f4d\u5740\uff08\u4f8b\u5982 1a2b3c\uff09", - "cat": "\u8a2d\u5099\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x10\uff09", - "subcat": "\u8a2d\u5099\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x0a\uff09" + "address": "\u88dd\u7f6e\u4f4d\u5740\uff08\u4f8b\u5982 1a2b3c\uff09", + "cat": "\u88dd\u7f6e\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x10\uff09", + "subcat": "\u88dd\u7f6e\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x0a\uff09" }, - "description": "\u65b0\u589e\u8a2d\u5099\u8986\u5beb\u3002", + "description": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", "title": "Insteon" }, "add_x10": { "data": { "housecode": "Housecode (a - p)", "platform": "\u5e73\u53f0", - "steps": "\u8abf\u5149\u968e\u6bb5\uff08\u50c5\u9069\u7528\u7167\u660e\u8a2d\u5099\u3001\u9810\u8a2d\u503c\u70ba 22\uff09", + "steps": "\u8abf\u5149\u968e\u6bb5\uff08\u50c5\u9069\u7528\u7167\u660e\u88dd\u7f6e\u3001\u9810\u8a2d\u503c\u70ba 22\uff09", "unitcode": "Unitcode (1 - 16)" }, "description": "\u8b8a\u66f4 Insteon Hub \u5bc6\u78bc\u3002", @@ -76,32 +76,32 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u8a2d\u5099\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", + "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", "title": "Insteon" }, "init": { "data": { - "add_override": "\u65b0\u589e\u8a2d\u5099\u8986\u5beb\u3002", - "add_x10": "\u65b0\u589e X10 \u8a2d\u5099\u3002", + "add_override": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", + "add_x10": "\u65b0\u589e X10 \u88dd\u7f6e\u3002", "change_hub_config": "\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3002", - "remove_override": "\u79fb\u9664\u8a2d\u5099\u8986\u5beb", - "remove_x10": "\u79fb\u9664 X10 \u8a2d\u5099\u3002" + "remove_override": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", + "remove_x10": "\u79fb\u9664 X10 \u88dd\u7f6e\u3002" }, "description": "\u9078\u64c7\u9078\u9805\u4ee5\u8a2d\u5b9a", "title": "Insteon" }, "remove_override": { "data": { - "address": "\u9078\u64c7\u8a2d\u5099\u4f4d\u5740\u4ee5\u79fb\u9664" + "address": "\u9078\u64c7\u88dd\u7f6e\u4f4d\u5740\u4ee5\u79fb\u9664" }, - "description": "\u79fb\u9664\u8a2d\u5099\u8986\u5beb", + "description": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", "title": "Insteon" }, "remove_x10": { "data": { - "address": "\u9078\u64c7\u8a2d\u5099\u4f4d\u5740\u4ee5\u79fb\u9664" + "address": "\u9078\u64c7\u88dd\u7f6e\u4f4d\u5740\u4ee5\u79fb\u9664" }, - "description": "\u79fb\u9664 X10 \u8a2d\u5099", + "description": "\u79fb\u9664 X10 \u88dd\u7f6e", "title": "Insteon" } } diff --git a/homeassistant/components/ios/translations/zh-Hant.json b/homeassistant/components/ios/translations/zh-Hant.json index ea5e5afce2..aceb4ea78d 100644 --- a/homeassistant/components/ios/translations/zh-Hant.json +++ b/homeassistant/components/ios/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ipma/translations/zh-Hans.json b/homeassistant/components/ipma/translations/zh-Hans.json index 7e0da1fb84..cd5d576d0a 100644 --- a/homeassistant/components/ipma/translations/zh-Hans.json +++ b/homeassistant/components/ipma/translations/zh-Hans.json @@ -15,5 +15,10 @@ "title": "\u4f4d\u7f6e" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u53ef\u8bbf\u95ee IPMA API" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index 9fcb91c462..f5d4446def 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002", "ipp_error": "\u767c\u751f IPP \u932f\u8aa4\u3002", "ipp_version_error": "\u4e0d\u652f\u63f4\u5370\u8868\u6a5f\u7684 IPP \u7248\u672c\u3002", "parse_error": "\u7372\u5f97\u5370\u8868\u6a5f\u56de\u61c9\u5931\u6557\u3002", - "unique_id_required": "\u8a2d\u5099\u7f3a\u5c11\u641c\u5c0b\u6240\u9700\u7368\u4e00\u8b58\u5225\u3002" + "unique_id_required": "\u88dd\u7f6e\u7f3a\u5c11\u641c\u5c0b\u6240\u9700\u7368\u4e00\u8b58\u5225\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json index b94faab25c..ea7a2c4f9b 100644 --- a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json +++ b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 43b43661b6..9ab55c19a7 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -32,7 +32,7 @@ "sensor_string": "\u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32", "variable_sensor_string": "\u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32" }, - "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u8a2d\u5099\u90fd\u6703\u88ab\u8996\u70ba\u50b3\u611f\u5668\u6216\u4e8c\u9032\u4f4d\u50b3\u611f\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u8a2d\u5099\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u50b3\u611f\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u8a2d\u5099\u9810\u8a2d\u4eae\u5ea6\u3002", + "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u50b3\u611f\u5668\u6216\u4e8c\u9032\u4f4d\u50b3\u611f\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u50b3\u611f\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", "title": "ISY994 \u9078\u9805" } } diff --git a/homeassistant/components/izone/translations/zh-Hant.json b/homeassistant/components/izone/translations/zh-Hant.json index f49de8669d..363e62a1b5 100644 --- a/homeassistant/components/izone/translations/zh-Hant.json +++ b/homeassistant/components/izone/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index a4aaf90934..11d962f9d1 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_uuid": "Kodi \u5be6\u4f8b\u6c92\u6709\u552f\u4e00 ID\u3002\u901a\u5e38\u662f\u56e0\u70ba Kodi \u7248\u672c\u904e\u820a\uff08\u4f4e\u65bc 17.x\uff09\u3002\u53ef\u4ee5\u624b\u52d5\u8a2d\u5b9a\u6574\u5408\u6216\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Kodi\u3002", diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index e4b38a6d10..604dc28b57 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099", + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -12,11 +12,11 @@ "step": { "confirm": { "description": "\u578b\u865f\uff1a{model}\nID\uff1a{id}\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" + "title": "Konnected \u88dd\u7f6e\u5df2\u5099\u59a5" }, "import_confirm": { "description": "\u65bc configuration.yaml \u4e2d\u767c\u73fe Konnected \u8b66\u5831 ID {id}\u3002\u6b64\u6d41\u7a0b\u5c07\u5141\u8a31\u532f\u5165\u81f3\u8a2d\u5b9a\u4e2d\u3002", - "title": "\u532f\u5165 Konnected \u8a2d\u5099" + "title": "\u532f\u5165 Konnected \u88dd\u7f6e" }, "user": { "data": { @@ -29,7 +29,7 @@ }, "options": { "abort": { - "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099" + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e" }, "error": { "bad_host": "\u7121\u6548\u7684\u8986\u5beb API \u4e3b\u6a5f\u7aef URL" diff --git a/homeassistant/components/kulersky/translations/cs.json b/homeassistant/components/kulersky/translations/cs.json new file mode 100644 index 0000000000..d3f0e37a13 --- /dev/null +++ b/homeassistant/components/kulersky/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/et.json b/homeassistant/components/kulersky/translations/et.json new file mode 100644 index 0000000000..9e7bb472e0 --- /dev/null +++ b/homeassistant/components/kulersky/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ru.json b/homeassistant/components/kulersky/translations/ru.json index 46de86e5ec..85a42bf1be 100644 --- a/homeassistant/components/kulersky/translations/ru.json +++ b/homeassistant/components/kulersky/translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "no_devices_found": "\u041d\u0438\u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0441\u0435\u0442\u0438.", - "single_instance_allowed": "\u0423\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u0412\u043e\u0437\u043c\u043e\u0436\u043da \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "step": { "confirm": { - "description": "\u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u044b?" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" } } } diff --git a/homeassistant/components/kulersky/translations/zh-Hant.json b/homeassistant/components/kulersky/translations/zh-Hant.json new file mode 100644 index 0000000000..90c98e491d --- /dev/null +++ b/homeassistant/components/kulersky/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index ed704711a6..154e82ec30 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/local_ip/translations/zh-Hant.json b/homeassistant/components/local_ip/translations/zh-Hant.json index d0238ff743..b14abdd6b6 100644 --- a/homeassistant/components/local_ip/translations/zh-Hant.json +++ b/homeassistant/components/local_ip/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/zh-Hant.json b/homeassistant/components/locative/translations/zh-Hant.json index 65dc4ff8da..8c2dcdb53e 100644 --- a/homeassistant/components/locative/translations/zh-Hant.json +++ b/homeassistant/components/locative/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/lovelace/translations/et.json b/homeassistant/components/lovelace/translations/et.json index 15c253dd4d..b5a1552efc 100644 --- a/homeassistant/components/lovelace/translations/et.json +++ b/homeassistant/components/lovelace/translations/et.json @@ -1,10 +1,10 @@ { "system_health": { "info": { - "dashboards": "Vaated", + "dashboards": "Vaateid", "mode": "Re\u017eiim", "resources": "Ressursid", - "views": "Vaated" + "views": "Paneele" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/zh-Hans.json b/homeassistant/components/lovelace/translations/zh-Hans.json index a30b7b2518..5807cdd7c1 100644 --- a/homeassistant/components/lovelace/translations/zh-Hans.json +++ b/homeassistant/components/lovelace/translations/zh-Hans.json @@ -1,10 +1,10 @@ { "system_health": { "info": { - "dashboards": "\u4eea\u8868\u76d8", + "dashboards": "\u4eea\u8868\u76d8\u6570\u91cf", "mode": "\u6a21\u5f0f", - "resources": "\u8d44\u6e90", - "views": "\u89c6\u56fe" + "resources": "\u8d44\u6e90\u6570\u91cf", + "views": "\u89c6\u56fe\u6570\u91cf" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index ab4e0832ed..4e8df0d5e9 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/mailgun/translations/zh-Hant.json b/homeassistant/components/mailgun/translations/zh-Hant.json index 19c41241a5..508a652ce9 100644 --- a/homeassistant/components/mailgun/translations/zh-Hant.json +++ b/homeassistant/components/mailgun/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/mikrotik/translations/zh-Hant.json b/homeassistant/components/mikrotik/translations/zh-Hant.json index 0675ede61b..6c3049eff0 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hant.json +++ b/homeassistant/components/mikrotik/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index e653bda920..b54a678398 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -18,7 +18,7 @@ "source_5": "\u4f86\u6e90 #5 \u540d\u7a31", "source_6": "\u4f86\u6e90 #6 \u540d\u7a31" }, - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } }, diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 8c8d23b565..37925ca628 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 586d96a78a..53d6d391e8 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -15,7 +15,7 @@ "port": "Port", "username": "Kasutajanimi" }, - "description": "Sisestage oma MQTT vahendaja andmed." + "description": "Sisesta oma MQTT vahendaja andmed." }, "hassio_confirm": { "data": { @@ -62,7 +62,7 @@ "port": "Port", "username": "Kasutajanimi" }, - "description": "Sisestage oma MQTT vahendaja \u00fchenduse teave." + "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave." }, "options": { "data": { diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index de92aee317..bfb2736188 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index 2c5c8c61fe..602b3d47dc 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "create_entry": { diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index bd72b659ae..931b8aa770 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -25,5 +25,13 @@ "title": "Koppel Nest-account" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Beweging gedetecteerd", + "camera_person": "Persoon gedetecteerd", + "camera_sound": "Geluid gedetecteerd", + "doorbell_chime": "Deurbel is ingedrukt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 80d0f8ee66..b097aec56a 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 590082b826..eab1d9741a 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -15,6 +15,13 @@ }, "options": { "step": { + "public_weather": { + "data": { + "area_name": "Naam van het gebied", + "mode": "Berekening", + "show_on_map": "Toon op kaart" + } + }, "public_weather_areas": { "description": "Configureer openbare weersensoren." } diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index 588675c670..e396deabb6 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index 34450afc84..0dc0931afe 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 5066f5a2ed..7b480bcc0f 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -15,7 +15,7 @@ "api_key": "API \u5bc6\u9470", "url": "\u7db2\u5740" }, - "description": "- URL\uff1aNightscout \u8a2d\u5099\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u5bc6\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u8a2d\u5099\u70ba\u4fdd\u8b77\u72c0\u614b\uff08(auth_default_roles != readable\uff09\u4e0b\u4f7f\u7528\u3002", + "description": "- URL\uff1aNightscout \u88dd\u7f6e\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u5bc6\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u88dd\u7f6e\u70ba\u4fdd\u8b77\u72c0\u614b\uff08(auth_default_roles != readable\uff09\u4e0b\u4f7f\u7528\u3002", "title": "\u8f38\u5165 Nightscout \u4f3a\u670d\u5668\u8cc7\u8a0a\u3002" } } diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index 86e059df4f..4b6597725e 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Deze gebruikersnaam is al in gebruik." }, "error": { + "invalid_auth": "Ongeldige authenticatie", "no_devices": "Geen apparaten gevonden in account" }, "step": { diff --git a/homeassistant/components/notion/translations/zh-Hant.json b/homeassistant/components/notion/translations/zh-Hant.json index 12bc209815..865bd1dbd0 100644 --- a/homeassistant/components/notion/translations/zh-Hant.json +++ b/homeassistant/components/notion/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" }, "step": { "user": { diff --git a/homeassistant/components/nuheat/translations/zh-Hant.json b/homeassistant/components/nuheat/translations/zh-Hant.json index eac51c4cf8..d04a5b165b 100644 --- a/homeassistant/components/nuheat/translations/zh-Hant.json +++ b/homeassistant/components/nuheat/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index b75bd37958..7c65e836f9 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index cc7d8071c2..f5f1bfd39e 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -21,5 +21,14 @@ "title": "Maak verbinding met NZBGet" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie (seconden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json index 7858092db4..26fd5f3411 100644 --- a/homeassistant/components/nzbget/translations/zh-Hant.json +++ b/homeassistant/components/nzbget/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/omnilogic/translations/nl.json b/homeassistant/components/omnilogic/translations/nl.json index 1127dc941e..5189795ec9 100644 --- a/homeassistant/components/omnilogic/translations/nl.json +++ b/homeassistant/components/omnilogic/translations/nl.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Polling-interval (in seconden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json index 99b5a46570..c2c39e00d6 100644 --- a/homeassistant/components/omnilogic/translations/zh-Hant.json +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index 8b2702b670..ae155ccf2c 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -2,6 +2,20 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_path": "Directory niet gevonden." + }, + "step": { + "owserver": { + "title": "Owserver-details instellen" + }, + "user": { + "data": { + "type": "Verbindingstype" + }, + "title": "Stel 1-Wire in" + } } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index acafb6eee4..9c606534a5 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_path": "\u672a\u627e\u5230\u8a2d\u5099\u3002" + "invalid_path": "\u672a\u627e\u5230\u88dd\u7f6e\u3002" }, "step": { "owserver": { diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index 6541d8accd..b21982fede 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "no_h264": "\u8a72\u8a2d\u5099\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a2d\u5b9a\u3002", - "no_mac": "\u7121\u6cd5\u70ba ONVIF \u8a2d\u5099\u8a2d\u5b9a\u552f\u4e00 ID\u3002", - "onvif_error": "\u8a2d\u5b9a ONVIF \u8a2d\u5099\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + "no_h264": "\u8a72\u88dd\u7f6e\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a2d\u5b9a\u3002", + "no_mac": "\u7121\u6cd5\u70ba ONVIF \u88dd\u7f6e\u8a2d\u5b9a\u552f\u4e00 ID\u3002", + "onvif_error": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -27,9 +27,9 @@ }, "device": { "data": { - "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u8a2d\u5099" + "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u88dd\u7f6e" }, - "title": "\u9078\u64c7 ONVIF \u8a2d\u5099" + "title": "\u9078\u64c7 ONVIF \u88dd\u7f6e" }, "manual_input": { "data": { @@ -37,11 +37,11 @@ "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a ONVIF \u8a2d\u5099" + "title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e" }, "user": { - "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u8a2d\u5099\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002", - "title": "ONVIF \u8a2d\u5099\u8a2d\u5b9a" + "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u88dd\u7f6e\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002", + "title": "ONVIF \u88dd\u7f6e\u8a2d\u5b9a" } } }, @@ -52,7 +52,7 @@ "extra_arguments": "\u984d\u5916 FFMPEG \u53c3\u6578", "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a" }, - "title": "ONVIF \u8a2d\u5099\u9078\u9805" + "title": "ONVIF \u88dd\u7f6e\u9078\u9805" } } } diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index 35099f2a59..ea138287c7 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" }, diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json index cf9f264a61..daa12f9e56 100644 --- a/homeassistant/components/ovo_energy/translations/nl.json +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -5,6 +5,9 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth": { + "title": "Opnieuw verifi\u00ebren" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json index f557a83009..43f456f757 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hant.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -19,7 +19,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8a2d\u5b9a OVO Energy \u8a2d\u5099\u4ee5\u76e3\u63a7\u80fd\u6e90\u4f7f\u7528\u72c0\u6cc1\u3002", + "description": "\u8a2d\u5b9a OVO Energy \u88dd\u7f6e\u4ee5\u76e3\u63a7\u80fd\u6e90\u4f7f\u7528\u72c0\u6cc1\u3002", "title": "\u65b0\u589e OVO Energy \u5e33\u865f" } } diff --git a/homeassistant/components/owntracks/translations/zh-Hant.json b/homeassistant/components/owntracks/translations/zh-Hant.json index b89f147247..6c92b55779 100644 --- a/homeassistant/components/owntracks/translations/zh-Hant.json +++ b/homeassistant/components/owntracks/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/ozw/translations/cs.json b/homeassistant/components/ozw/translations/cs.json index 621e48bab7..b9cc99e72b 100644 --- a/homeassistant/components/ozw/translations/cs.json +++ b/homeassistant/components/ozw/translations/cs.json @@ -4,6 +4,8 @@ "addon_info_failed": "Nepoda\u0159ilo se z\u00edskat informace o dopl\u0148ku OpenZWave.", "addon_install_failed": "Instalace dopl\u0148ku OpenZWave se nezda\u0159ila.", "addon_set_config_failed": "Nepoda\u0159ilo se nastavit OpenZWave.", + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "mqtt_required": "Integrace MQTT nen\u00ed nastavena", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index f4334e1d63..f9ed5469da 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -4,8 +4,10 @@ "addon_info_failed": "\u53d6\u5f97 OpenZWave add-on \u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "OpenZWave add-on \u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "OpenZWave add-on \u8a2d\u5b9a\u5931\u6557\u3002", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "addon_start_failed": "OpenZWave add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" @@ -14,6 +16,9 @@ "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" }, "step": { + "hassio_confirm": { + "title": "\u4ee5 OpenZWave add-on \u8a2d\u5b9a OpenZwave \u6574\u5408" + }, "install_addon": { "title": "OpenZWave add-on \u5b89\u88dd\u5df2\u555f\u52d5" }, @@ -27,7 +32,7 @@ "start_addon": { "data": { "network_key": "\u7db2\u8def\u5bc6\u9470", - "usb_path": "USB \u8a2d\u5099\u8def\u5f91" + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8acb\u8f38\u5165 OpenZWave \u8a2d\u5b9a\u3002" } diff --git a/homeassistant/components/panasonic_viera/translations/zh-Hant.json b/homeassistant/components/panasonic_viera/translations/zh-Hant.json index 5ac554d269..1b39556f45 100644 --- a/homeassistant/components/panasonic_viera/translations/zh-Hant.json +++ b/homeassistant/components/panasonic_viera/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index dbfe2075e2..aec745ea38 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index bbab02f959..710d363f77 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 3841a173df..93a99ba1d3 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 8cfa8bdb46..45edbf2d88 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/profiler/translations/zh-Hant.json b/homeassistant/components/profiler/translations/zh-Hant.json index 85797ab208..c7d73c344d 100644 --- a/homeassistant/components/profiler/translations/zh-Hant.json +++ b/homeassistant/components/profiler/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json index 13a968114a..815ee581e6 100644 --- a/homeassistant/components/progettihwsw/translations/zh-Hant.json +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index ddfcbef493..77bfa7bfdb 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002" }, @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u8a2d\u5099\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "\u540d\u7a31", "region": "\u5340\u57df" }, - "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN \u78bc\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u8a2d\u5099\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN \u78bc\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u8a2d\u5099\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } } diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index f8dbde4691..b800daee77 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -14,7 +14,7 @@ "api_key": "API \u5bc6\u9470" }, "description": "\u5c07\u6703\u9700\u8981\u7531 https://app.rach.io/ \u53d6\u5f97 App \u5bc6\u9470\u3002\u9078\u64c7\u8a2d\u5b9a\u4e26\u9078\u64c7\u7372\u5f97\u5bc6\u9470\uff08GET API KEY\uff09\u3002", - "title": "\u9023\u7dda\u81f3 Rachio \u8a2d\u5099" + "title": "\u9023\u7dda\u81f3 Rachio \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index cefc44956c..9b5829cf20 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 7ce887b05c..b40bc6d197 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548" diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 827def7f30..3da2e5f538 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -17,13 +17,13 @@ }, "setup_serial": { "data": { - "device": "\u9078\u64c7\u8a2d\u5099" + "device": "\u9078\u64c7\u88dd\u7f6e" }, - "title": "\u8a2d\u5099" + "title": "\u88dd\u7f6e" }, "setup_serial_manual_path": { "data": { - "device": "USB \u8a2d\u5099\u8def\u5f91" + "device": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8def\u5f91" }, @@ -37,7 +37,7 @@ }, "options": { "error": { - "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_event_code": "\u4e8b\u4ef6\u4ee3\u78bc\u7121\u6548", "invalid_input_2262_off": "\u547d\u4ee4\u95dc\u9589\u8f38\u5165\u7121\u6548", "invalid_input_2262_on": "\u547d\u4ee4\u958b\u555f\u8f38\u5165\u7121\u6548", @@ -49,9 +49,9 @@ "data": { "automatic_add": "\u958b\u555f\u81ea\u52d5\u65b0\u589e", "debug": "\u958b\u555f\u9664\u932f", - "device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a", + "device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e", - "remove_device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u522a\u9664" + "remove_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u522a\u9664" }, "title": "Rfxtrx \u9078\u9805" }, @@ -60,13 +60,13 @@ "command_off": "\u547d\u4ee4\u95dc\u9589\u7684\u8cc7\u6599\u4f4d\u5143\u503c", "command_on": "\u547d\u4ee4\u958b\u555f\u7684\u8cc7\u6599\u4f4d\u5143\u503c", "data_bit": "\u8cc7\u6599\u4f4d\u5143\u6578", - "fire_event": "\u958b\u555f\u8a2d\u5099\u4e8b\u4ef6", + "fire_event": "\u958b\u555f\u88dd\u7f6e\u4e8b\u4ef6", "off_delay": "\u5ef6\u9072", "off_delay_enabled": "\u958b\u555f\u5ef6\u9072", - "replace_device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u53d6\u4ee3", + "replace_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u53d6\u4ee3", "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578" }, - "title": "\u8a2d\u5b9a\u8a2d\u5099\u9078\u9805" + "title": "\u8a2d\u5b9a\u88dd\u7f6e\u9078\u9805" } } } diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json index b9a66540c3..5eb31bfa79 100644 --- a/homeassistant/components/ring/translations/zh-Hant.json +++ b/homeassistant/components/ring/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index 9509ace654..c76871bcec 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 94e6d6cb48..cfa3a4aa3b 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 794c67454f..932e5cadd7 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -13,7 +13,7 @@ "password": "\u5bc6\u78bc" }, "description": "\u76ee\u524d\u63a5\u6536 BLID \u8207\u5bc6\u78bc\u70ba\u624b\u52d5\u904e\u7a0b\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1ahttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } }, diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index 00b152205f..f34bce445f 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", diff --git a/homeassistant/components/rpi_power/translations/zh-Hant.json b/homeassistant/components/rpi_power/translations/zh-Hant.json index 37dbb151d8..05cdeb6852 100644 --- a/homeassistant/components/rpi_power/translations/zh-Hant.json +++ b/homeassistant/components/rpi_power/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u627e\u4e0d\u5230\u7cfb\u7d71\u6240\u9700\u7684\u5143\u4ef6\uff0c\u8acb\u78ba\u5b9a Kernel \u70ba\u6700\u65b0\u7248\u672c\u3001\u540c\u6642\u786c\u9ad4\u70ba\u652f\u63f4\u72c0\u614b", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json index 8bf65ef6ee..cad7d736a9 100644 --- a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json +++ b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index e932e18a2b..00b442399c 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "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", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json index 356e58f640..d819bfd4bb 100644 --- a/homeassistant/components/sense/translations/zh-Hant.json +++ b/homeassistant/components/sense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index a63efaf6dc..b73a2e57f1 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "bad_dsn": "DSN \u7121\u6548", diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index 59ac0f5ccc..bf0150523b 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "unsupported_firmware": "\u8a2d\u5099\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unsupported_firmware": "\u88dd\u7f6e\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f\n\n\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u8a2d\u5099\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u8a2d\u5099\u3002" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f\n\n\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002" }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u8a2d\u5099\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u8a2d\u5099\u3002" + "description": "\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/smappee/translations/et.json b/homeassistant/components/smappee/translations/et.json index 4996f9dea9..37a10c69ec 100644 --- a/homeassistant/components/smappee/translations/et.json +++ b/homeassistant/components/smappee/translations/et.json @@ -21,7 +21,7 @@ "data": { "host": "" }, - "description": "Smappee kohaliku sidumise algatamiseks sisestage hostinimi" + "description": "Smappee kohaliku sidumise algatamiseks sisesta hostinimi" }, "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 77f726a0dc..4f41b5a1e5 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_configured_local_device": "\u672c\u5730\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\uff0c\u8acb\u5148\u9032\u884c\u79fb\u9664\u5f8c\u518d\u8a2d\u5b9a\u96f2\u7aef\u8a2d\u5099\u3002", + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_local_device": "\u672c\u5730\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\uff0c\u8acb\u5148\u9032\u884c\u79fb\u9664\u5f8c\u518d\u8a2d\u5b9a\u96f2\u7aef\u88dd\u7f6e\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_mdns": "Smappee \u6574\u5408\u4e0d\u652f\u63f4\u7684\u8a2d\u5099\u3002", + "invalid_mdns": "Smappee \u6574\u5408\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, @@ -27,8 +27,8 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u8a2d\u5099\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u8a2d\u5099" + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u88dd\u7f6e" } } } diff --git a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json index df467dd38b..d232b491b6 100644 --- a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json +++ b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/sms/translations/zh-Hant.json b/homeassistant/components/sms/translations/zh-Hant.json index 30951f88d0..35952af999 100644 --- a/homeassistant/components/sms/translations/zh-Hant.json +++ b/homeassistant/components/sms/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "device": "\u8a2d\u5099" + "device": "\u88dd\u7f6e" }, "title": "\u9023\u7dda\u81f3\u6578\u64da\u6a5f" } diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index 01c1db919c..18cf04cf5a 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API", "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/solarlog/translations/zh-Hant.json b/homeassistant/components/solarlog/translations/zh-Hant.json index b8f53a74ff..b97772a8d4 100644 --- a/homeassistant/components/solarlog/translations/zh-Hant.json +++ b/homeassistant/components/solarlog/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { diff --git a/homeassistant/components/soma/translations/zh-Hant.json b/homeassistant/components/soma/translations/zh-Hant.json index 1665930404..3dfb164955 100644 --- a/homeassistant/components/soma/translations/zh-Hant.json +++ b/homeassistant/components/soma/translations/zh-Hant.json @@ -8,7 +8,7 @@ "result_error": "SOMA \u9023\u7dda\u56de\u61c9\u72c0\u614b\u932f\u8aa4\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json index 4768da884d..71390930e3 100644 --- a/homeassistant/components/somfy/translations/zh-Hant.json +++ b/homeassistant/components/somfy/translations/zh-Hant.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/songpal/translations/zh-Hant.json b/homeassistant/components/songpal/translations/zh-Hant.json index b73aaade30..ddb334d754 100644 --- a/homeassistant/components/songpal/translations/zh-Hant.json +++ b/homeassistant/components/songpal/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_songpal_device": "\u4e26\u975e Songpal \u8a2d\u5099" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_songpal_device": "\u4e26\u975e Songpal \u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/sonos/translations/zh-Hant.json b/homeassistant/components/sonos/translations/zh-Hant.json index b47280e3a9..31a9d3ce95 100644 --- a/homeassistant/components/sonos/translations/zh-Hant.json +++ b/homeassistant/components/sonos/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index e9459bf08f..e88b4ec392 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "wrong_server_id": "\u4f3a\u670d\u5668 ID \u7121\u6548" }, "step": { diff --git a/homeassistant/components/spider/translations/zh-Hant.json b/homeassistant/components/spider/translations/zh-Hant.json index 96b9ad519d..ce15c28f47 100644 --- a/homeassistant/components/spider/translations/zh-Hant.json +++ b/homeassistant/components/spider/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/spotify/translations/zh-Hans.json b/homeassistant/components/spotify/translations/zh-Hans.json new file mode 100644 index 0000000000..19a6909de4 --- /dev/null +++ b/homeassistant/components/spotify/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "api_endpoint_reachable": "\u53ef\u8bbf\u95ee Spotify API" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/zh-Hant.json b/homeassistant/components/squeezebox/translations/zh-Hant.json index 85942f812b..067374f6c1 100644 --- a/homeassistant/components/squeezebox/translations/zh-Hant.json +++ b/homeassistant/components/squeezebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_server_found": "\u627e\u4e0d\u5230 LMS \u4f3a\u670d\u5668\u3002" }, "error": { diff --git a/homeassistant/components/srp_energy/translations/zh-Hant.json b/homeassistant/components/srp_energy/translations/zh-Hant.json index f8cb25f7df..87bf347795 100644 --- a/homeassistant/components/srp_energy/translations/zh-Hant.json +++ b/homeassistant/components/srp_energy/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/syncthru/translations/zh-Hant.json b/homeassistant/components/syncthru/translations/zh-Hant.json index a31ea74fb0..fbbc85c4a1 100644 --- a/homeassistant/components/syncthru/translations/zh-Hant.json +++ b/homeassistant/components/syncthru/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_url": "\u7db2\u5740\u7121\u6548", - "syncthru_not_supported": "\u8a2d\u5099\u4e0d\u652f\u63f4 SyncThru", + "syncthru_not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4 SyncThru", "unknown_state": "\u5370\u8868\u6a5f\u72c0\u614b\u672a\u77e5\uff0c\u8acb\u78ba\u8a8d URL \u8207\u7db2\u8def\u9023\u7dda" }, "flow_title": "Samsung SyncThru \u5370\u8868\u6a5f\uff1a{name}", diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 4f50be2795..d5e78faf91 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tado/translations/zh-Hant.json b/homeassistant/components/tado/translations/zh-Hant.json index 59e2d80c56..9126e0e4ea 100644 --- a/homeassistant/components/tado/translations/zh-Hant.json +++ b/homeassistant/components/tado/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index 1431fc8e1b..477eb0ffa9 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_discovery_topic": "\u63a2\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json index daf5ff0ec1..46f6f6cf16 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -5,7 +5,7 @@ "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002", + "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u88dd\u7f6e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index e88d982b8a..2fac2ac142 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f" } } } diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json index 71d22d66cb..2204e7c332 100644 --- a/homeassistant/components/traccar/translations/zh-Hant.json +++ b/homeassistant/components/traccar/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/tradfri/translations/zh-Hant.json b/homeassistant/components/tradfri/translations/zh-Hant.json index 21b232b757..9a48c1bc52 100644 --- a/homeassistant/components/tradfri/translations/zh-Hant.json +++ b/homeassistant/components/tradfri/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index fc75254a9e..5329ceb31e 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 52f502b546..6031af445a 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -17,7 +17,7 @@ "platform": "\u00c4pp kus teie konto registreeriti", "username": "Kasutajanimi" }, - "description": "Sisestage oma Tuya konto andmed.", + "description": "Sisesta oma Tuya konto andmed.", "title": "" } } diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index f2fb4c0926..3735d12e61 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" @@ -24,14 +24,14 @@ }, "options": { "error": { - "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", - "dev_not_config": "\u8a2d\u5099\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", - "dev_not_found": "\u8a2d\u5099\u627e\u4e0d\u5230" + "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", + "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", + "dev_not_found": "\u627e\u4e0d\u5230\u88dd\u7f6e" }, "step": { "device": { "data": { - "brightness_range_mode": "\u8a2d\u5099\u6240\u4f7f\u7528\u4e4b\u4eae\u5ea6\u7bc4\u570d", + "brightness_range_mode": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u4eae\u5ea6\u7bc4\u570d", "curr_temp_divider": "\u76ee\u524d\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", "max_kelvin": "Kelvin \u652f\u63f4\u6700\u9ad8\u8272\u6eab", "max_temp": "\u6700\u9ad8\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", @@ -39,18 +39,18 @@ "min_temp": "\u6700\u4f4e\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", "support_color": "\u5f37\u5236\u8272\u6eab\u652f\u63f4", "temp_divider": "\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", - "tuya_max_coltemp": "\u8a2d\u5099\u56de\u5831\u6700\u9ad8\u8272\u6eab", - "unit_of_measurement": "\u8a2d\u5099\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" + "tuya_max_coltemp": "\u88dd\u7f6e\u56de\u5831\u6700\u9ad8\u8272\u6eab", + "unit_of_measurement": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" }, - "description": "\u8a2d\u5b9a\u9078\u9805\u4ee5\u8abf\u6574 {device_type} \u8a2d\u5099 `{device_name}` \u986f\u793a\u8cc7\u8a0a", - "title": "\u8a2d\u5b9a Tuya \u8a2d\u5099" + "description": "\u8a2d\u5b9a\u9078\u9805\u4ee5\u8abf\u6574 {device_type} \u88dd\u7f6e `{device_name}` \u986f\u793a\u8cc7\u8a0a", + "title": "\u8a2d\u5b9a Tuya \u88dd\u7f6e" }, "init": { "data": { - "discovery_interval": "\u63a2\u7d22\u8a2d\u5099\u66f4\u65b0\u79d2\u9593\u8ddd", - "list_devices": "\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", - "query_device": "\u9078\u64c7\u8a2d\u5099\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", - "query_interval": "\u67e5\u8a62\u8a2d\u5099\u66f4\u65b0\u79d2\u9593\u8ddd" + "discovery_interval": "\u63a2\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", + "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", + "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", + "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" }, "description": "\u66f4\u65b0\u9593\u8ddd\u4e0d\u8981\u8a2d\u5b9a\u7684\u904e\u4f4e\u3001\u53ef\u80fd\u6703\u5c0e\u81f4\u65bc\u65e5\u8a8c\u4e2d\u7522\u751f\u932f\u8aa4\u8a0a\u606f", "title": "\u8a2d\u5b9a Tuya \u9078\u9805" diff --git a/homeassistant/components/twilio/translations/zh-Hant.json b/homeassistant/components/twilio/translations/zh-Hant.json index 630afb0297..0776d7cb0e 100644 --- a/homeassistant/components/twilio/translations/zh-Hant.json +++ b/homeassistant/components/twilio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json index a325d458ac..7e6a113e1e 100644 --- a/homeassistant/components/twinkly/translations/zh-Hant.json +++ b/homeassistant/components/twinkly/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "device_exists": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "device_exists": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Twinkly \u8a2d\u5099\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09" + "host": "Twinkly \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09" }, "description": "\u8a2d\u5b9a Twinkly LED \u71c8\u4e32", "title": "Twinkly" diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index b17ad0b551..d87f8cf51e 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -39,17 +39,17 @@ "ignore_wired_bug": "\u95dc\u9589 UniFi \u6709\u7dda\u932f\u8aa4\u908f\u8f2f", "ssid_filter": "\u9078\u64c7\u6240\u8981\u8ffd\u8e64\u7684\u7121\u7dda\u7db2\u8def", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" }, - "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64", + "description": "\u8a2d\u5b9a\u88dd\u7f6e\u8ffd\u8e64", "title": "UniFi \u9078\u9805 1/3" }, "simple_options": { "data": { "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09" + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09" }, "description": "\u8a2d\u5b9a UniFi \u6574\u5408" }, diff --git a/homeassistant/components/upb/translations/zh-Hant.json b/homeassistant/components/upb/translations/zh-Hant.json index e4809c9b63..b121c005fa 100644 --- a/homeassistant/components/upb/translations/zh-Hant.json +++ b/homeassistant/components/upb/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index 008b007e2f..64423efed3 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "incomplete_discovery": "\u672a\u5b8c\u6210\u63a2\u7d22", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "flow_title": "UPnP/IGD\uff1a{name}", "step": { "ssdp_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u8a2d\u5099\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u88dd\u7f6e\uff1f" }, "user": { "data": { "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", - "usn": "\u8a2d\u5099" + "usn": "\u88dd\u7f6e" } } } diff --git a/homeassistant/components/velbus/translations/zh-Hant.json b/homeassistant/components/velbus/translations/zh-Hant.json index 28469cd1d9..f9bbe99d9c 100644 --- a/homeassistant/components/velbus/translations/zh-Hant.json +++ b/homeassistant/components/velbus/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { diff --git a/homeassistant/components/vera/translations/et.json b/homeassistant/components/vera/translations/et.json index 9695048458..4afb098caa 100644 --- a/homeassistant/components/vera/translations/et.json +++ b/homeassistant/components/vera/translations/et.json @@ -22,7 +22,7 @@ "exclude": "Vera seadme ID-d mida Home Assistant'ist v\u00e4lja j\u00e4tta.", "lights": "Vera l\u00fclitite ID'd mida neid k\u00e4sitleda Home Assistantis tuledena." }, - "description": "Valikuliste parameetrite kohta leiad lisateavet Vera dokumentatsioonist: https://www.home-assistant.io/integrations/vera/. M\u00e4rkus: K\u00f5ikide muudatuste puhul on vaja taask\u00e4ivitada Home Assistant'i server. V\u00e4\u00e4rtuste kustutamiseks sisestage t\u00fchik.", + "description": "Valikuliste parameetrite kohta leiad lisateavet Vera dokumentatsioonist: https://www.home-assistant.io/integrations/vera/. M\u00e4rkus: K\u00f5ikide muudatuste puhul on vaja taask\u00e4ivitada Home Assistant'i server. V\u00e4\u00e4rtuste kustutamiseks sisesta t\u00fchik.", "title": "Vera kontrolleri valikud" } } diff --git a/homeassistant/components/vera/translations/zh-Hant.json b/homeassistant/components/vera/translations/zh-Hant.json index 7293ce9761..b8d7031ee1 100644 --- a/homeassistant/components/vera/translations/zh-Hant.json +++ b/homeassistant/components/vera/translations/zh-Hant.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u8a2d\u5099 ID\u3002", - "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u8a2d\u5099 ID\u3002", + "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u88dd\u7f6e ID\u3002", + "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u88dd\u7f6e ID\u3002", "vera_controller_url": "\u63a7\u5236\u5668 URL" }, "description": "\u65bc\u4e0b\u65b9\u63d0\u4f9b Vera \u63a7\u5236\u5668 URL\u3002\u683c\u5f0f\u61c9\u8a72\u70ba\uff1ahttp://192.168.1.161:3480\u3002", @@ -19,8 +19,8 @@ "step": { "init": { "data": { - "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u8a2d\u5099 ID\u3002", - "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u8a2d\u5099 ID\u3002" + "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u88dd\u7f6e ID\u3002", + "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u88dd\u7f6e ID\u3002" }, "description": "\u8acb\u53c3\u95b1 Vera \u6587\u4ef6\u4ee5\u7372\u5f97\u8a73\u7d30\u7684\u9078\u9805\u53c3\u6578\u8cc7\u6599\uff1ahttps://www.home-assistant.io/integrations/vera/\u3002\u8acb\u6ce8\u610f\uff1a\u4efb\u4f55\u8b8a\u66f4\u90fd\u9700\u8981\u91cd\u555f Home Assistant\u3002\u6b32\u6e05\u9664\u8a2d\u5b9a\u503c\u3001\u8acb\u8f38\u5165\u7a7a\u683c\u3002", "title": "Vera \u63a7\u5236\u5668\u9078\u9805" diff --git a/homeassistant/components/vesync/translations/zh-Hant.json b/homeassistant/components/vesync/translations/zh-Hant.json index 02cffeefc4..264ad237af 100644 --- a/homeassistant/components/vesync/translations/zh-Hant.json +++ b/homeassistant/components/vesync/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json index abbc12e6d8..b266e25b39 100644 --- a/homeassistant/components/vilfo/translations/zh-Hant.json +++ b/homeassistant/components/vilfo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index 74d6a858d8..257ed829b6 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "updated_entry": "\u6b64\u5be6\u9ad4\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u5be6\u9ad4\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "complete_pairing_failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", - "existing_config_entry_found": "\u5df2\u6709\u4e00\u7d44\u4f7f\u7528\u76f8\u540c\u5e8f\u865f\u7684 VIZIO SmartCast \u8a2d\u5099 \u5df2\u8a2d\u5b9a\u3002\u5fc5\u9808\u5148\u9032\u884c\u522a\u9664\u5f8c\u624d\u80fd\u91cd\u65b0\u8a2d\u5b9a\u3002" + "existing_config_entry_found": "\u5df2\u6709\u4e00\u7d44\u4f7f\u7528\u76f8\u540c\u5e8f\u865f\u7684 VIZIO SmartCast \u88dd\u7f6e \u5df2\u8a2d\u5b9a\u3002\u5fc5\u9808\u5148\u9032\u884c\u522a\u9664\u5f8c\u624d\u80fd\u91cd\u65b0\u8a2d\u5b9a\u3002" }, "step": { "pair_tv": { @@ -19,22 +19,22 @@ "title": "\u5b8c\u6210\u914d\u5c0d\u904e\u7a0b" }, "pairing_complete": { - "description": "VIZIO SmartCast \u8a2d\u5099 \u5df2\u7d93\u9023\u7dda\u81f3 Home Assistant\u3002", + "description": "VIZIO SmartCast \u88dd\u7f6e \u5df2\u7d93\u9023\u7dda\u81f3 Home Assistant\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "pairing_complete_import": { - "description": "VIZIO SmartCast \u8a2d\u5099 \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", + "description": "VIZIO SmartCast \u88dd\u7f6e \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "user": { "data": { "access_token": "\u5b58\u53d6\u5bc6\u9470", - "device_class": "\u8a2d\u5099\u985e\u5225", + "device_class": "\u88dd\u7f6e\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u5bc6\u9470 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", - "title": "VIZIO SmartCast \u8a2d\u5099" + "title": "VIZIO SmartCast \u88dd\u7f6e" } } }, @@ -47,7 +47,7 @@ "volume_step": "\u97f3\u91cf\u5927\u5c0f" }, "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", - "title": "\u66f4\u65b0 VIZIO SmartCast \u8a2d\u5099 \u9078\u9805" + "title": "\u66f4\u65b0 VIZIO SmartCast \u88dd\u7f6e \u9078\u9805" } } } diff --git a/homeassistant/components/volumio/translations/zh-Hant.json b/homeassistant/components/volumio/translations/zh-Hant.json index 48f3ad6d17..f557397372 100644 --- a/homeassistant/components/volumio/translations/zh-Hant.json +++ b/homeassistant/components/volumio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u5df2\u63a2\u7d22\u5230\u7684 Volumio" }, "error": { diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json index 0b9966135b..4be8350847 100644 --- a/homeassistant/components/wemo/translations/zh-Hant.json +++ b/homeassistant/components/wemo/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/wiffi/translations/zh-Hant.json b/homeassistant/components/wiffi/translations/zh-Hant.json index ae2956cc5e..ea02e17933 100644 --- a/homeassistant/components/wiffi/translations/zh-Hant.json +++ b/homeassistant/components/wiffi/translations/zh-Hant.json @@ -9,7 +9,7 @@ "data": { "port": "\u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a WIFFI \u8a2d\u5099 TCP \u4f3a\u670d\u5668" + "title": "\u8a2d\u5b9a WIFFI \u88dd\u7f6e TCP \u4f3a\u670d\u5668" } } }, diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index 8859e831d5..0a86501c8f 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u8a2d\u5099\u3002", - "not_wilight_device": "\u6b64\u8a2d\u5099\u4e26\u975e WiLight" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u88dd\u7f6e\u3002", + "not_wilight_device": "\u6b64\u88dd\u7f6e\u4e26\u975e WiLight" }, "flow_title": "WiLight\uff1a{name}", "step": { diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 394a42c5fd..cd917f42b4 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -7,7 +7,7 @@ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 37c74d07f5..0073bb2248 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -16,8 +16,8 @@ "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u8a2d\u5099\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u8a2d\u5099" + "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u88dd\u7f6e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u88dd\u7f6e" } } } diff --git a/homeassistant/components/wolflink/translations/zh-Hant.json b/homeassistant/components/wolflink/translations/zh-Hant.json index 13eb90b55d..2a0dbc2544 100644 --- a/homeassistant/components/wolflink/translations/zh-Hant.json +++ b/homeassistant/components/wolflink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -11,9 +11,9 @@ "step": { "device": { "data": { - "device_name": "\u8a2d\u5099" + "device_name": "\u88dd\u7f6e" }, - "title": "\u9078\u64c7 WOLF \u8a2d\u5099" + "title": "\u9078\u64c7 WOLF \u88dd\u7f6e" }, "user": { "data": { diff --git a/homeassistant/components/xbox/translations/zh-Hant.json b/homeassistant/components/xbox/translations/zh-Hant.json index 477bd13374..07fc710408 100644 --- a/homeassistant/components/xbox/translations/zh-Hant.json +++ b/homeassistant/components/xbox/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index cd1059436d..582aea354c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u8a2d\u5099\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" + "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" }, "error": { - "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u8a2d\u5099\u7684 IP \u4f5c\u70ba\u4ecb\u9762", + "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", @@ -26,7 +26,7 @@ "key": "\u7db2\u95dc\u5bc6\u9470", "name": "\u7db2\u95dc\u540d\u7a31" }, - "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u50b3\u611f\u5668\u8a2d\u5099\u7684\u8cc7\u8a0a\u3002\uff3c", + "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u50b3\u611f\u5668\u88dd\u7f6e\u7684\u8cc7\u8a0a", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\u9078\u9805\u8a2d\u5b9a" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index fd77eb4df8..95499fb7b8 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_device_selected": "\u672a\u9078\u64c7\u8a2d\u5099\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u8a2d\u5099\u3002" + "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002" }, "flow_title": "Xiaomi Miio\uff1a{name}", "step": { @@ -23,7 +23,7 @@ "data": { "gateway": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" }, - "description": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u8a2d\u5099\u3002", + "description": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u88dd\u7f6e\u3002", "title": "\u5c0f\u7c73 Miio" } } diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index b19ebdb40f..d9bf3c123b 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -10,14 +10,14 @@ "step": { "pick_device": { "data": { - "device": "\u8a2d\u5099" + "device": "\u88dd\u7f6e" } }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u8a2d\u5099\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } }, diff --git a/homeassistant/components/zerproc/translations/zh-Hant.json b/homeassistant/components/zerproc/translations/zh-Hant.json index 91a0dc60be..90c98e491d 100644 --- a/homeassistant/components/zerproc/translations/zh-Hant.json +++ b/homeassistant/components/zerproc/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index e62d61ac8e..6582507431 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -18,14 +18,14 @@ "data": { "baudrate": "\u901a\u8a0a\u57e0\u901f\u5ea6", "flow_control": "\u8cc7\u6599\u6d41\u91cf\u63a7\u5236", - "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91" + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" }, "description": "\u8f38\u5165\u901a\u8a0a\u57e0\u7279\u5b9a\u8a2d\u5b9a", "title": "\u8a2d\u5b9a" }, "user": { "data": { - "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91" + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" }, "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", "title": "ZHA" @@ -62,14 +62,14 @@ "turn_on": "\u958b\u555f" }, "trigger_type": { - "device_dropped": "\u8a2d\u5099\u6389\u843d", - "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u8a2d\u5099", - "device_knocked": "\u6572\u64ca \"{subtype}\" \u8a2d\u5099", - "device_offline": "\u8a2d\u5099\u96e2\u7dda", - "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u8a2d\u5099", - "device_shaken": "\u8a2d\u5099\u6416\u6643", - "device_slid": "\u63a8\u52d5 \"{subtype}\" \u8a2d\u5099", - "device_tilted": "\u8a2d\u5099\u540d\u7a31", + "device_dropped": "\u88dd\u7f6e\u6389\u843d", + "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u88dd\u7f6e", + "device_knocked": "\u6572\u64ca \"{subtype}\" \u88dd\u7f6e", + "device_offline": "\u88dd\u7f6e\u96e2\u7dda", + "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u88dd\u7f6e", + "device_shaken": "\u88dd\u7f6e\u6416\u6643", + "device_slid": "\u63a8\u52d5 \"{subtype}\" \u88dd\u7f6e", + "device_tilted": "\u88dd\u7f6e\u540d\u7a31", "remote_button_alt_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca\u9375\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", "remote_button_alt_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", "remote_button_alt_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index fdb263fd5f..f5c07a9efc 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "usb_path": "USB \u8a2d\u5099\u8def\u5f91" + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/", "title": "\u8a2d\u5b9a Z-Wave" From 916ff887744c8e073c4166b3ede91f71dc8a6e73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Dec 2020 03:16:37 +0100 Subject: [PATCH 017/302] Bump hatasmota to 0.1.4 (#43912) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 205 ++++++++++++++++++ 4 files changed, 208 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a4d6ec6036..a4c6f77fc1 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.2"], + "requirements": ["hatasmota==0.1.4"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 55fc295705..35703034d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b100b4fe96..e5552f4c45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 627eb5198a..f09c27da75 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -493,6 +493,191 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_OFF +async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["so"]["17"] = 0 # Hex color in state updates + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 255, 128) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + +async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["ty"] = 1 # Tuya device + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -745,6 +930,26 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->100: Speed should be capped at 40 + await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->0: Speed should be 1 + await common.async_turn_on(hass, "light.test", brightness=0, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 1;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->50: Speed should be 4*2*2=16 await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( From 52edf6719daae7a79106fb7adb3936e89c9aba9c Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Thu, 3 Dec 2020 21:57:35 -0500 Subject: [PATCH 018/302] Bump auroranoaa library to 0.0.2 (#43898) --- homeassistant/components/aurora/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 20f9e82dcb..8d7d856e50 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/aurora", "config_flow": true, "codeowners": ["@djtimca"], - "requirements": ["auroranoaa==0.0.1"] + "requirements": ["auroranoaa==0.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35703034d3..59ef44f8fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -291,7 +291,7 @@ asyncpysupla==0.0.5 atenpdu==0.3.0 # homeassistant.components.aurora -auroranoaa==0.0.1 +auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5552f4c45..472941a3da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ arcam-fmj==0.5.3 async-upnp-client==0.14.13 # homeassistant.components.aurora -auroranoaa==0.0.1 +auroranoaa==0.0.2 # homeassistant.components.stream av==8.0.2 From a5279cc2795522e4776305e3a94d92d4f87df925 Mon Sep 17 00:00:00 2001 From: Jonas Lundberg Date: Fri, 4 Dec 2020 04:27:25 +0100 Subject: [PATCH 019/302] Upgrade respx to 0.16.2 (#43892) * Bump respx to 0.16.2 * Align sensor tests to use new respx mock api --- requirements_test.txt | 2 +- tests/components/rest/test_binary_sensor.py | 43 +++++------ tests/components/rest/test_sensor.py | 85 +++++++++------------ 3 files changed, 54 insertions(+), 76 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8ec5a611f1..e4553a2498 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,6 +24,6 @@ pytest-xdist==2.1.0 pytest==6.1.2 requests_mock==1.8.0 responses==0.12.0 -respx==0.14.0 +respx==0.16.2 stdlib-list==0.7.0 tqdm==4.49.0 diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 48d13a716a..2638477ef7 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch +from tests.async_mock import patch async def test_setup_missing_basic_config(hass): @@ -50,9 +50,7 @@ async def test_setup_missing_config(hass): @respx.mock async def test_setup_failed_connect(hass): """Test setup when connection error occurs.""" - respx.get( - "http://localhost", content=httpx.RequestError(message="any", request=Mock()) - ) + respx.get("http://localhost").mock(side_effect=httpx.RequestError) assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -71,7 +69,7 @@ async def test_setup_failed_connect(hass): @respx.mock async def test_setup_timeout(hass): """Test setup when connection timeout occurs.""" - respx.get("http://localhost", content=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -90,7 +88,7 @@ async def test_setup_timeout(hass): @respx.mock async def test_setup_minimum(hass): """Test setup with minimum configuration.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -109,7 +107,7 @@ async def test_setup_minimum(hass): @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -127,7 +125,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock async def test_setup_duplicate_resource_template(hass): """Test setup with duplicate resources.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -146,7 +144,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock async def test_setup_get(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -174,7 +172,7 @@ async def test_setup_get(hass): @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -202,7 +200,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock async def test_setup_post(hass): """Test setup with valid configuration.""" - respx.post("http://localhost", status_code=200, content="{}") + respx.post("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -230,11 +228,10 @@ async def test_setup_post(hass): @respx.mock async def test_setup_get_off(hass): """Test setup with valid off configuration.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/json"}, - content='{"dog": false}', + json={"dog": False}, ) assert await async_setup_component( hass, @@ -261,11 +258,10 @@ async def test_setup_get_off(hass): @respx.mock async def test_setup_get_on(hass): """Test setup with valid on configuration.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/json"}, - content='{"dog": true}', + json={"dog": True}, ) assert await async_setup_component( hass, @@ -292,7 +288,7 @@ async def test_setup_get_on(hass): @respx.mock async def test_setup_with_exception(hass): """Test setup with exception.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -318,9 +314,7 @@ async def test_setup_with_exception(hass): await hass.async_block_till_done() respx.clear() - respx.get( - "http://localhost", content=httpx.RequestError(message="any", request=Mock()) - ) + respx.get("http://localhost").mock(side_effect=httpx.RequestError) await hass.services.async_call( "homeassistant", "update_entity", @@ -337,7 +331,7 @@ async def test_setup_with_exception(hass): async def test_reload(hass): """Verify we can reload reset sensors.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 await async_setup_component( hass, @@ -380,10 +374,7 @@ async def test_reload(hass): @respx.mock async def test_setup_query_params(hass): """Test setup with query params.""" - respx.get( - "http://localhost?search=something", - status_code=200, - ) + respx.get("http://localhost", params={"search": "something"}) % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 71bcbedda8..f378a1fc2a 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch +from tests.async_mock import patch async def test_setup_missing_config(hass): @@ -42,9 +42,7 @@ async def test_setup_missing_schema(hass): @respx.mock async def test_setup_failed_connect(hass): """Test setup when connection error occurs.""" - respx.get( - "http://localhost", content=httpx.RequestError(message="any", request=Mock()) - ) + respx.get("http://localhost").mock(side_effect=httpx.RequestError) assert await async_setup_component( hass, sensor.DOMAIN, @@ -63,7 +61,7 @@ async def test_setup_failed_connect(hass): @respx.mock async def test_setup_timeout(hass): """Test setup when connection timeout occurs.""" - respx.get("http://localhost", content=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, sensor.DOMAIN, @@ -76,7 +74,7 @@ async def test_setup_timeout(hass): @respx.mock async def test_setup_minimum(hass): """Test setup with minimum configuration.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -95,7 +93,7 @@ async def test_setup_minimum(hass): @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -113,7 +111,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock async def test_setup_duplicate_resource_template(hass): """Test setup with duplicate resources.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -132,7 +130,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock async def test_setup_get(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "sensor", @@ -161,7 +159,7 @@ async def test_setup_get(hass): @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "sensor", @@ -190,7 +188,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock async def test_setup_post(hass): """Test setup with valid configuration.""" - respx.post("http://localhost", status_code=200, content="{}") + respx.post("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "sensor", @@ -219,8 +217,7 @@ async def test_setup_post(hass): @respx.mock async def test_setup_get_xml(hass): """Test setup with valid xml configuration.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="abc", @@ -252,10 +249,7 @@ async def test_setup_get_xml(hass): @respx.mock async def test_setup_query_params(hass): """Test setup with query params.""" - respx.get( - "http://localhost?search=something", - status_code=200, - ) + respx.get("http://localhost", params={"search": "something"}) % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -276,11 +270,9 @@ async def test_setup_query_params(hass): async def test_update_with_json_attrs(hass): """Test attributes get extracted from a JSON result.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='{ "key": "some_json_value" }', + json={"key": "some_json_value"}, ) assert await async_setup_component( hass, @@ -311,11 +303,9 @@ async def test_update_with_json_attrs(hass): async def test_update_with_no_template(hass): """Test update when there is no value template.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='{ "key": "some_json_value" }', + json={"key": "some_json_value"}, ) assert await async_setup_component( hass, @@ -338,15 +328,14 @@ async def test_update_with_no_template(hass): assert len(hass.states.async_all()) == 1 state = hass.states.get("sensor.foo") - assert state.state == '{ "key": "some_json_value" }' + assert state.state == '{"key": "some_json_value"}' @respx.mock async def test_update_with_json_attrs_no_data(hass, caplog): """Test attributes when no JSON result fetched.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": CONTENT_TYPE_JSON}, content="", @@ -382,11 +371,9 @@ async def test_update_with_json_attrs_no_data(hass, caplog): async def test_update_with_json_attrs_not_dict(hass, caplog): """Test attributes get extracted from a JSON result.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='["list", "of", "things"]', + json=["list", "of", "things"], ) assert await async_setup_component( hass, @@ -419,8 +406,7 @@ async def test_update_with_json_attrs_not_dict(hass, caplog): async def test_update_with_json_attrs_bad_JSON(hass, caplog): """Test attributes get extracted from a JSON result.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": CONTENT_TYPE_JSON}, content="This is text rather than JSON data.", @@ -456,11 +442,17 @@ async def test_update_with_json_attrs_bad_JSON(hass, caplog): async def test_update_with_json_attrs_with_json_attrs_path(hass): """Test attributes get extracted from a JSON result with a template for the attributes.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }', + json={ + "toplevel": { + "master_value": "master", + "second_level": { + "some_json_key": "some_json_value", + "some_json_key2": "some_json_value2", + }, + }, + }, ) assert await async_setup_component( hass, @@ -494,8 +486,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass): async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="mastersome_json_valuesome_json_value2", @@ -531,8 +522,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): """Test attributes get extracted from a JSON result that was converted from XML.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content='01255648alexander000bogus000000000upupupup000x0XF0x0XF 0', @@ -573,8 +563,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp ): """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "application/xml"}, content="
13
", @@ -610,8 +599,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp async def test_update_with_xml_convert_bad_xml(hass, caplog): """Test attributes get extracted from a XML result with bad xml.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="", @@ -646,8 +634,7 @@ async def test_update_with_xml_convert_bad_xml(hass, caplog): async def test_update_with_failed_get(hass, caplog): """Test attributes get extracted from a XML result with bad xml.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="", @@ -682,7 +669,7 @@ async def test_update_with_failed_get(hass, caplog): async def test_reload(hass): """Verify we can reload reset sensors.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 await async_setup_component( hass, From 5e6f5fca90cc9b050fcd010c56aa540f6097f594 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Dec 2020 04:39:49 +0100 Subject: [PATCH 020/302] Don't send MQTT birth message in tests (#43917) --- tests/components/mqtt/test_init.py | 12 ++++++++++++ tests/conftest.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 86b905f2b0..82a88de918 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -781,6 +781,18 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) +@pytest.mark.parametrize( + "mqtt_config", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "homeassistant/status", + mqtt.ATTR_PAYLOAD: "online", + }, + } + ], +) async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test sending birth message.""" birth = asyncio.Event() diff --git a/tests/conftest.py b/tests/conftest.py index fa390f9bf3..d8fb9f2914 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -400,7 +400,7 @@ def mqtt_client_mock(hass): async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): """Fixture to mock MQTT component.""" if mqtt_config is None: - mqtt_config = {mqtt.CONF_BROKER: "mock-broker"} + mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} result = await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: mqtt_config}) assert result From a47cf27ed6a9028e1d1af8e186452b0140fee8bd Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Dec 2020 20:23:20 +0100 Subject: [PATCH 021/302] Always send ozw network key to add-on config (#43938) --- homeassistant/components/ozw/config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 4543bc2798..887560b154 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -147,9 +147,10 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.network_key = user_input[CONF_NETWORK_KEY] self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = {CONF_ADDON_DEVICE: self.usb_path} - if self.network_key: - new_addon_config[CONF_ADDON_NETWORK_KEY] = self.network_key + new_addon_config = { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + } if new_addon_config != self.addon_config: await self._async_set_addon_config(new_addon_config) From e23dc90bacffa296f8fb4feb64d8f63212101779 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Dec 2020 20:41:08 +0100 Subject: [PATCH 022/302] Handle stale ozw discovery flow (#43939) --- homeassistant/components/ozw/config_flow.py | 12 ++-- tests/components/ozw/test_config_flow.py | 66 +++++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 887560b154..7c7c6e65df 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -58,17 +58,14 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - addon_config = await self._async_get_addon_config() - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return await self.async_step_hassio_confirm() async def async_step_hassio_confirm(self, user_input=None): """Confirm the add-on discovery.""" if user_input is not None: - self.use_addon = True - return self._async_create_entry_from_vars() + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) return self.async_show_form(step_id="hassio_confirm") @@ -107,6 +104,9 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True if await self._async_is_addon_running(): + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") return self._async_create_entry_from_vars() if await self._async_is_addon_installed(): diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 289b6c7f4c..e86232adc6 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -159,9 +159,10 @@ async def test_not_addon(hass, supervisor): assert len(mock_setup_entry.mock_calls) == 1 -async def test_addon_running(hass, supervisor, addon_running): +async def test_addon_running(hass, supervisor, addon_running, addon_options): """Test add-on already running on Supervisor.""" - hass.config.components.add("mqtt") + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -182,8 +183,8 @@ async def test_addon_running(hass, supervisor, addon_running): assert result["type"] == "create_entry" assert result["title"] == TITLE assert result["data"] == { - "usb_path": None, - "network_key": None, + "usb_path": "/test", + "network_key": "abc123", "use_addon": True, "integration_created_addon": False, } @@ -193,7 +194,6 @@ async def test_addon_running(hass, supervisor, addon_running): async def test_addon_info_failure(hass, supervisor, addon_info): """Test add-on info failure.""" - hass.config.components.add("mqtt") addon_info.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -213,7 +213,6 @@ async def test_addon_installed( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on already installed but not running on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -250,7 +249,6 @@ async def test_set_addon_config_failure( hass, supervisor, addon_installed, addon_options, set_addon_options ): """Test add-on set config failure.""" - hass.config.components.add("mqtt") set_addon_options.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -273,7 +271,6 @@ async def test_start_addon_failure( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on start failure.""" - hass.config.components.add("mqtt") start_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -302,7 +299,6 @@ async def test_addon_not_installed( start_addon, ): """Test add-on not installed.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None await setup.async_setup_component(hass, "persistent_notification", {}) @@ -348,7 +344,6 @@ async def test_addon_not_installed( async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): """Test add-on install failure.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -488,3 +483,54 @@ async def test_abort_discovery_with_existing_entry( assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_discovery_addon_not_running( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test discovery with add-on already installed but not running.""" + addon_options["device"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "start_addon" + assert result["type"] == "form" + + +async def test_discovery_addon_not_installed( + hass, supervisor, addon_installed, install_addon, addon_options +): + """Test discovery with add-on not installed.""" + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "install_addon" + assert result["type"] == "progress" + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" From b19c7058674266ba8bdfed8733c9bc3dcaf04230 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Dec 2020 23:04:31 +0100 Subject: [PATCH 023/302] Updated frontend to 20201204.0 (#43945) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1bf6cdc580..65fe745e51 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201203.0"], + "requirements": ["home-assistant-frontend==20201204.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 65f228f5a0..d820f5e715 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 59ef44f8fa..1148222aaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 472941a3da..e451e137dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 125ceb7449f5e2ee650fe06977105ae1b8fc2155 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 4 Dec 2020 18:45:09 -0500 Subject: [PATCH 024/302] Refactor ZHA core channel initialization (#43953) * Cleanup Basic channnel Remove unused methods. * Refactor async_configure() method Split async_configure() into async_configure() and async_configure_channel_specfici() * Refactor async_initilize() method Split into two different methods and configure channel specifics via async_configure_channel_specific() * Fixes --- homeassistant/components/zha/binary_sensor.py | 12 ++---- .../components/zha/core/channels/base.py | 17 +++++--- .../components/zha/core/channels/closures.py | 10 ----- .../components/zha/core/channels/general.py | 43 ++++--------------- .../zha/core/channels/homeautomation.py | 16 +++---- .../components/zha/core/channels/hvac.py | 3 +- .../components/zha/core/channels/lighting.py | 12 +++--- .../components/zha/core/channels/security.py | 22 ++++------ .../zha/core/channels/smartenergy.py | 12 +++--- 9 files changed, 49 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index ba95a0e4bc..48f35e035f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -73,13 +73,9 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): self._channel = channels[0] self._device_class = self.DEVICE_CLASS - async def get_device_class(self): - """Get the HA device class from the channel.""" - async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - await self.get_device_class() self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) @@ -168,10 +164,10 @@ class IASZone(BinarySensor): SENSOR_ATTR = "zone_status" - async def get_device_class(self) -> None: - """Get the HA device class from the channel.""" - zone_type = await self._channel.get_attribute_value("zone_type") - self._device_class = CLASS_MAPPING.get(zone_type) + @property + def device_class(self) -> str: + """Return device class from component DEVICE_CLASSES.""" + return CLASS_MAPPING.get(self._channel.cluster.get("zone_type")) async def async_update(self): """Attempt to retrieve on off state from the binary sensor.""" diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index c6019c1084..2dbd162948 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -187,29 +187,36 @@ class ZigbeeChannel(LogMixin): str(ex), ) - async def async_configure(self): + async def async_configure(self) -> None: """Set cluster binding and attribute reporting.""" if not self._ch_pool.skip_configuration: await self.bind() if self.cluster.is_server: await self.configure_reporting() + ch_specific_cfg = getattr(self, "async_configure_channel_specific", None) + if ch_specific_cfg: + await ch_specific_cfg() self.debug("finished channel configuration") else: self.debug("skipping channel configuration") self._status = ChannelStatus.CONFIGURED - async def async_initialize(self, from_cache): + async def async_initialize(self, from_cache: bool) -> None: """Initialize channel.""" if not from_cache and self._ch_pool.skip_configuration: self._status = ChannelStatus.INITIALIZED return self.debug("initializing channel: from_cache: %s", from_cache) - attributes = [] - for report_config in self._report_config: - attributes.append(report_config["attr"]) + attributes = [cfg["attr"] for cfg in self._report_config] if attributes: await self.get_attributes(attributes, from_cache=from_cache) + + ch_specific_init = getattr(self, "async_initialize_channel_specific", None) + if ch_specific_init: + await ch_specific_init(from_cache=from_cache) + + self.debug("finished channel configuration") self._status = ChannelStatus.INITIALIZED @callback diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 0760427d46..0326f18ac6 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -35,11 +35,6 @@ class DoorLockChannel(ZigbeeChannel): f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self._value_attribute, from_cache=from_cache) - await super().async_initialize(from_cache) - @registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) class Shade(ZigbeeChannel): @@ -85,8 +80,3 @@ class WindowCovering(ZigbeeChannel): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self._value_attribute, from_cache=from_cache) - await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index dc06d01e59..fa4883ae5a 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -1,6 +1,6 @@ """General channels module for Zigbee Home Automation.""" import asyncio -from typing import Any, List, Optional +from typing import Any, Coroutine, List, Optional import zigpy.exceptions import zigpy.zcl.clusters.general as general @@ -19,7 +19,7 @@ from ..const import ( SIGNAL_SET_LEVEL, SIGNAL_UPDATE_DEVICE, ) -from .base import ChannelStatus, ClientChannel, ZigbeeChannel, parse_and_log_command +from .base import ClientChannel, ZigbeeChannel, parse_and_log_command @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) @@ -71,21 +71,6 @@ class BasicChannel(ZigbeeChannel): 6: "Emergency mains and transfer switch", } - async def async_configure(self): - """Configure this channel.""" - await super().async_configure() - await self.async_initialize(False) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - if not self._ch_pool.skip_configuration or from_cache: - await self.get_attribute_value("power_source", from_cache=from_cache) - await super().async_initialize(from_cache) - - def get_power_source(self): - """Get the power source.""" - return self.cluster.get("power_source") - @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) class BinaryInput(ZigbeeChannel): @@ -189,11 +174,6 @@ class LevelControlChannel(ZigbeeChannel): """Dispatch level change.""" self.async_send_signal(f"{self.unique_id}_{command}", level) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self.CURRENT_LEVEL, from_cache=from_cache) - await super().async_initialize(from_cache) - @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) class MultistateInput(ZigbeeChannel): @@ -284,12 +264,9 @@ class OnOffChannel(ZigbeeChannel): ) self._state = bool(value) - async def async_initialize(self, from_cache): + async def async_initialize_channel_specific(self, from_cache: bool) -> None: """Initialize channel.""" - await super().async_initialize(from_cache) - state = await self.get_attribute_value(self.ON_OFF, from_cache=True) - if state is not None: - self._state = bool(state) + self._state = self.on_off async def async_update(self): """Initialize channel.""" @@ -338,7 +315,7 @@ class PollControl(ZigbeeChannel): CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s LONG_POLL = 6 * 4 # 6s - async def async_configure(self) -> None: + async def async_configure_channel_specific(self) -> None: """Configure channel: set check-in interval.""" try: res = await self.cluster.write_attributes( @@ -347,7 +324,6 @@ class PollControl(ZigbeeChannel): self.debug("%ss check-in interval set: %s", self.CHECKIN_INTERVAL / 4, res) except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex: self.debug("Couldn't set check-in interval: %s", ex) - await super().async_configure() @callback def cluster_command( @@ -375,16 +351,13 @@ class PowerConfigurationChannel(ZigbeeChannel): {"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE}, ) - async def async_initialize(self, from_cache): - """Initialize channel.""" + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: + """Initialize channel specific attrs.""" attributes = [ "battery_size", - "battery_percentage_remaining", - "battery_voltage", "battery_quantity", ] - await self.get_attributes(attributes, from_cache=from_cache) - self._status = ChannelStatus.INITIALIZED + return self.get_attributes(attributes, from_cache=from_cache) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 03812be054..5b3a4778fc 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -1,5 +1,5 @@ """Home automation channels module for Zigbee Home Automation.""" -from typing import Optional +from typing import Coroutine, Optional import zigpy.zcl.clusters.homeautomation as homeautomation @@ -62,23 +62,17 @@ class ElectricalMeasurementChannel(ZigbeeChannel): result, ) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.fetch_config(True) - await super().async_initialize(from_cache) + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: + """Initialize channel specific attributes.""" - async def fetch_config(self, from_cache): - """Fetch config from device and updates format specifier.""" - - # prime the cache - await self.get_attributes( + return self.get_attributes( [ "ac_power_divisor", "power_divisor", "ac_power_multiplier", "power_multiplier", ], - from_cache=from_cache, + from_cache=True, ) @property diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index ac832aacc6..f8f04414fa 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -362,7 +362,7 @@ class ThermostatChannel(ZigbeeChannel): ) @retryable_req(delays=(1, 1, 3)) - async def async_initialize(self, from_cache): + async def async_initialize_channel_specific(self, from_cache: bool) -> None: """Initialize channel.""" cached = [a for a, cached in self._init_attrs.items() if cached] @@ -370,7 +370,6 @@ class ThermostatChannel(ZigbeeChannel): await self._chunk_attr_read(cached, cached=True) await self._chunk_attr_read(uncached, cached=False) - await super().async_initialize(from_cache) async def async_set_operation_mode(self, mode) -> bool: """Set Operation mode.""" diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 16223582c3..c8827e20e0 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,5 +1,5 @@ """Lighting channels module for Zigbee Home Automation.""" -from typing import Optional +from typing import Coroutine, Optional import zigpy.zcl.clusters.lighting as lighting @@ -75,15 +75,13 @@ class ColorChannel(ZigbeeChannel): """Return the warmest color_temp that this channel supports.""" return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) - async def async_configure(self) -> None: + def async_configure_channel_specific(self) -> Coroutine: """Configure channel.""" - await self.fetch_color_capabilities(False) - await super().async_configure() + return self.fetch_color_capabilities(False) - async def async_initialize(self, from_cache: bool) -> None: + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: """Initialize channel.""" - await self.fetch_color_capabilities(True) - await super().async_initialize(from_cache) + return self.fetch_color_capabilities(True) async def fetch_color_capabilities(self, from_cache: bool) -> None: """Get the color configuration.""" diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index e37987bc82..7c600d9840 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ import asyncio +from typing import Coroutine from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.security as security @@ -20,7 +21,7 @@ from ..const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .base import ZigbeeChannel +from .base import ChannelStatus, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) @@ -155,14 +156,10 @@ class IASZoneChannel(ZigbeeChannel): str(ex), ) - try: - self.debug("Sending pro-active IAS enroll response") - await self._cluster.enroll_response(0, 0) - except ZigbeeException as ex: - self.debug( - "Failed to send pro-active IAS enroll response: %s", - str(ex), - ) + self.debug("Sending pro-active IAS enroll response") + self._cluster.create_catching_task(self._cluster.enroll_response(0, 0)) + + self._status = ChannelStatus.CONFIGURED self.debug("finished IASZoneChannel configuration") @callback @@ -177,8 +174,7 @@ class IASZoneChannel(ZigbeeChannel): value, ) - async def async_initialize(self, from_cache): + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: """Initialize channel.""" - attributes = ["zone_status", "zone_state"] - await self.get_attributes(attributes, from_cache=from_cache) - await super().async_initialize(from_cache) + attributes = ["zone_status", "zone_state", "zone_type"] + return self.get_attributes(attributes, from_cache=from_cache) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 792b941329..0bd7159cf9 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -1,5 +1,5 @@ """Smart energy channels module for Zigbee Home Automation.""" -from typing import Union +from typing import Coroutine, Union import zigpy.zcl.clusters.smartenergy as smartenergy @@ -96,15 +96,13 @@ class Metering(ZigbeeChannel): """Return multiplier for the value.""" return self.cluster.get("multiplier") - async def async_configure(self) -> None: + def async_configure_channel_specific(self) -> Coroutine: """Configure channel.""" - await self.fetch_config(False) - await super().async_configure() + return self.fetch_config(False) - async def async_initialize(self, from_cache: bool) -> None: + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: """Initialize channel.""" - await self.fetch_config(True) - await super().async_initialize(from_cache) + return self.fetch_config(True) @callback def attribute_updated(self, attrid: int, value: int) -> None: From 6e74f9013642edfc26bfbc68f08504f65d33d357 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 5 Dec 2020 00:03:50 +0000 Subject: [PATCH 025/302] [ci skip] Translation update --- .../accuweather/translations/es.json | 6 ++ .../accuweather/translations/it.json | 6 ++ .../accuweather/translations/lb.json | 5 ++ .../accuweather/translations/pl.json | 6 ++ .../accuweather/translations/ru.json | 3 +- .../components/airly/translations/es.json | 5 ++ .../components/airly/translations/it.json | 5 ++ .../components/airly/translations/lb.json | 5 ++ .../components/airly/translations/pl.json | 5 ++ .../components/apple_tv/translations/cs.json | 16 ++++- .../components/apple_tv/translations/es.json | 64 +++++++++++++++++++ .../components/apple_tv/translations/it.json | 64 +++++++++++++++++++ .../components/apple_tv/translations/lb.json | 6 ++ .../components/apple_tv/translations/no.json | 38 ++++++++++- .../components/apple_tv/translations/pl.json | 46 ++++++++++++- .../components/apple_tv/translations/ru.json | 12 +++- .../components/bsblan/translations/it.json | 4 +- .../components/hyperion/translations/it.json | 52 +++++++++++++++ .../components/ipma/translations/it.json | 5 ++ .../components/kulersky/translations/es.json | 13 ++++ .../components/kulersky/translations/it.json | 13 ++++ .../components/kulersky/translations/pl.json | 13 ++++ .../components/lovelace/translations/cs.json | 2 +- .../mobile_app/translations/it.json | 5 ++ .../components/nest/translations/it.json | 8 +++ .../components/ozw/translations/cs.json | 3 + .../components/ozw/translations/es.json | 5 ++ .../components/ozw/translations/it.json | 11 ++++ .../components/ozw/translations/pl.json | 5 ++ .../components/ozw/translations/ru.json | 4 +- 30 files changed, 422 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/apple_tv/translations/es.json create mode 100644 homeassistant/components/apple_tv/translations/it.json create mode 100644 homeassistant/components/apple_tv/translations/lb.json create mode 100644 homeassistant/components/hyperion/translations/it.json create mode 100644 homeassistant/components/kulersky/translations/es.json create mode 100644 homeassistant/components/kulersky/translations/it.json create mode 100644 homeassistant/components/kulersky/translations/pl.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 5d4522e8ce..aa24b5ff97 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -31,5 +31,11 @@ "title": "Opciones de AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor AccuWeather", + "remaining_requests": "Solicitudes permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json index 1c22344f55..86aaa213a1 100644 --- a/homeassistant/components/accuweather/translations/it.json +++ b/homeassistant/components/accuweather/translations/it.json @@ -31,5 +31,11 @@ "title": "Opzioni AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server AccuWeather", + "remaining_requests": "Richieste consentite rimanenti" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/lb.json b/homeassistant/components/accuweather/translations/lb.json index e1a9306e00..4d9689ccf5 100644 --- a/homeassistant/components/accuweather/translations/lb.json +++ b/homeassistant/components/accuweather/translations/lb.json @@ -31,5 +31,10 @@ "title": "AccuWeather Optiounen" } } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather Server ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 2ac9aabfc9..c6e4fb3ba8 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -31,5 +31,11 @@ "title": "Opcje AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera AccuWeather", + "remaining_requests": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 1d90958efe..6a675c1724 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -34,7 +34,8 @@ }, "system_health": { "info": { - "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 AccuWeather" + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 AccuWeather", + "remaining_requests": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index 4fb6a0905c..a0ed36a716 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index 6f3fd919df..bf6d7a461c 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/lb.json b/homeassistant/components/airly/translations/lb.json index 46eb3a91f0..dd24ee3066 100644 --- a/homeassistant/components/airly/translations/lb.json +++ b/homeassistant/components/airly/translations/lb.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Airly Server ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index f13c212e25..e36e6f86ec 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json index 412d31082f..59f83e0272 100644 --- a/homeassistant/components/apple_tv/translations/cs.json +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -12,16 +12,28 @@ "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, + "flow_title": "Apple TV: {name}", "step": { + "pair_no_pin": { + "title": "P\u00e1rov\u00e1n\u00ed" + }, "pair_with_pin": { "data": { "pin": "PIN k\u00f3d" - } + }, + "title": "P\u00e1rov\u00e1n\u00ed" + }, + "reconfigure": { + "description": "U t\u00e9to Apple TV doch\u00e1z\u00ed k probl\u00e9m\u016fm s p\u0159ipojen\u00edm a je t\u0159eba ji znovu nastavit." + }, + "service_problem": { + "title": "Nepoda\u0159ilo se p\u0159idat slu\u017ebu" }, "user": { "data": { "device_input": "Za\u0159\u00edzen\u00ed" - } + }, + "title": "Nastaven\u00ed nov\u00e9 Apple TV" } } }, diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json new file mode 100644 index 0000000000..d03a77ca1c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/es.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", + "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", + "invalid_config": "La configuraci\u00f3n para este dispositivo est\u00e1 incompleta. Intenta a\u00f1adirlo de nuevo.", + "no_devices_found": "No se encontraron dispositivos en la red", + "unknown": "Error inesperado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_usable_service": "Se encontr\u00f3 un dispositivo, pero no se pudo identificar ninguna manera de establecer una conexi\u00f3n con \u00e9l. Si sigues viendo este mensaje, intenta especificar su direcci\u00f3n IP o reiniciar el Apple TV.", + "unknown": "Error inesperado" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e1s a punto de a\u00f1adir el Apple TV con nombre `{name}` a Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", + "title": "Confirma la adici\u00f3n del Apple TV" + }, + "pair_no_pin": { + "description": "El emparejamiento es necesario para el servicio `{protocol}`. Introduce el PIN en tu Apple TV para continuar.", + "title": "Emparejamiento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", + "title": "Emparejamiento" + }, + "reconfigure": { + "description": "Este Apple TV est\u00e1 experimentando algunos problemas de conexi\u00f3n y debe ser reconfigurado.", + "title": "Reconfiguraci\u00f3n del dispositivo" + }, + "service_problem": { + "description": "Se ha producido un problema durante el protocolo de emparejamiento `{protocol}`. Ser\u00e1 ignorado.", + "title": "Error al a\u00f1adir el servicio" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Empieza introduciendo el nombre del dispositivo (eje. Cocina o Dormitorio) o la direcci\u00f3n IP del Apple TV que quieres a\u00f1adir. Si se han econtrado dispositivos en tu red, se mostrar\u00e1n a continuaci\u00f3n.\n\nSi no puedes ver el dispositivo o experimentas alg\u00fan problema, intente especificar la direcci\u00f3n IP del dispositivo.\n\n{devices}", + "title": "Configurar un nuevo Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "No encender el dispositivo al iniciar Home Assistant" + }, + "description": "Configurar los ajustes generales del dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json new file mode 100644 index 0000000000..3a4ae887aa --- /dev/null +++ b/homeassistant/components/apple_tv/translations/it.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "backoff": "Il dispositivo non accetta richieste di abbinamento in questo momento (potresti aver inserito un codice PIN non valido troppe volte), riprova pi\u00f9 tardi.", + "device_did_not_pair": "Nessun tentativo di completare il processo di abbinamento \u00e8 stato effettuato dal dispositivo.", + "invalid_config": "La configurazione per questo dispositivo \u00e8 incompleta. Prova ad aggiungerlo di nuovo.", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "unknown": "Errore imprevisto" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_auth": "Autenticazione non valida", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "no_usable_service": "\u00c8 stato trovato un dispositivo ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione ad esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", + "unknown": "Errore imprevisto" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Stai per aggiungere l'Apple TV denominata \"{name}\" a Home Assistant. \n\n **Per completare la procedura, potrebbe essere necessario inserire pi\u00f9 codici PIN.** \n\nTieni presente che *non* sarai in grado di spegnere la tua Apple TV con questa integrazione. Solo il lettore multimediale in Home Assistant si spegner\u00e0!", + "title": "Conferma l'aggiunta di Apple TV" + }, + "pair_no_pin": { + "description": "L'abbinamento \u00e8 richiesto per il servizio \"{protocol}\". Inserisci il PIN {pin} sulla tua Apple TV per continuare.", + "title": "Abbinamento" + }, + "pair_with_pin": { + "data": { + "pin": "Codice PIN" + }, + "description": "L'abbinamento \u00e8 richiesto per il {protocol} \"{protocol}\". Immettere il codice PIN visualizzato sullo schermo. Gli zeri iniziali devono essere omessi, ovvero immettere 123 se il codice visualizzato \u00e8 0123.", + "title": "Abbinamento" + }, + "reconfigure": { + "description": "Questa Apple TV sta riscontrando alcune difficolt\u00e0 di connessione e deve essere riconfigurata.", + "title": "Riconfigurazione del dispositivo" + }, + "service_problem": { + "description": "Si \u00e8 verificato un problema durante l'associazione del protocollo \"{protocol}\". Sar\u00e0 ignorato.", + "title": "Impossibile aggiungere il servizio" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Inizia inserendo il nome del dispositivo (es. Cucina o Camera da letto) o l'indirizzo IP dell'Apple TV che desideri aggiungere. Se sono stati rilevati automaticamente dei dispositivi sulla rete, verranno visualizzati di seguito. \n\n Se non riesci a vedere il tuo dispositivo o riscontri problemi, prova a specificare l'indirizzo IP del dispositivo. \n\n {devices}", + "title": "Configura una nuova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Non accendere il dispositivo all'avvio di Home Assistant" + }, + "description": "Configurare le impostazioni generali del dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json new file mode 100644 index 0000000000..bbfc9f7b31 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/lb.json @@ -0,0 +1,6 @@ +{ + "config": { + "flow_title": "Apple TV: {name}" + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index 27823ede2b..974fe2ce5f 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -3,6 +3,9 @@ "abort": { "already_configured_device": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "backoff": "Enheten godtar ikke parringsanmodninger for \u00f8yeblikket (du har kanskje angitt en ugyldig PIN-kode for mange ganger), pr\u00f8v igjen senere.", + "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten.", + "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "unknown": "Uventet feil" }, @@ -10,19 +13,50 @@ "already_configured": "Enheten er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_usable_service": "En enhet ble funnet, men kunne ikke identifisere noen m\u00e5te \u00e5 etablere en tilkobling til den. Hvis du fortsetter \u00e5 se denne meldingen, kan du pr\u00f8ve \u00e5 angi IP-adressen eller starte Apple TV p\u00e5 nytt.", "unknown": "Uventet feil" }, "flow_title": "", "step": { + "confirm": { + "description": "Du er i ferd med \u00e5 legge til Apple TV med navnet {name} i Home Assistant.\n\n**For \u00e5 fullf\u00f8re prosessen m\u00e5 du kanskje angi flere PIN-koder.**\n\nV\u00e6r oppmerksom p\u00e5 at du *ikke* kan sl\u00e5 av Apple TV med denne integreringen. Bare mediespilleren i Home Assistant sl\u00e5r seg av!", + "title": "Bekreft at du legger til Apple TV" + }, + "pair_no_pin": { + "description": "Paring kreves for tjenesten {protocol}. Skriv inn PIN-koden {pin} p\u00e5 Apple TV for \u00e5 fortsette.", + "title": "Sammenkobling" + }, "pair_with_pin": { "data": { "pin": "PIN kode" - } + }, + "description": "Paring kreves for protokollen {protocol}. Skriv inn PIN-koden som vises p\u00e5 skjermen. Ledende nuller utelates, det vil si angi 123 hvis den viste koden er 0123.", + "title": "Sammenkobling" + }, + "reconfigure": { + "description": "Denne Apple TV har noen tilkoblingsvansker og m\u00e5 konfigureres p\u00e5 nytt.", + "title": "Omkonfigurering av enheter" + }, + "service_problem": { + "description": "Det oppstod et problem under sammenkobling av protokollen \" {protocol} \". Det vil bli ignorert.", + "title": "Kunne ikke legge til tjenesten" }, "user": { "data": { "device_input": "Enhet" - } + }, + "description": "Start med \u00e5 skrive inn enhetsnavnet (f.eks. kj\u00f8kken eller soverom) eller IP-adressen til Apple TV-en du vil legge til. Hvis noen enheter ble funnet automatisk p\u00e5 nettverket ditt, vises de nedenfor.\n\nHvis du ikke kan se enheten eller oppleve problemer, kan du pr\u00f8ve \u00e5 angi enhetens IP-adresse.\n\n{devices}", + "title": "Konfigurere en ny Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Ikke sl\u00e5 p\u00e5 enheten n\u00e5r du starter Home Assistant" + }, + "description": "Konfigurer generelle enhetsinnstillinger" } } }, diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json index 2fb8d234ea..e8950d1c71 100644 --- a/homeassistant/components/apple_tv/translations/pl.json +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -15,6 +15,50 @@ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Zamierzasz doda\u0107 Apple TV o nazwie \"{name}\" do Home Assistanta. \n\n **Aby uko\u0144czy\u0107 ca\u0142y proces, mo\u017ce by\u0107 konieczne wprowadzenie wielu kod\u00f3w PIN.** \n\nPami\u0119taj, \u017ce \"NIE\" b\u0119dziesz w stanie wy\u0142\u0105czy\u0107 Apple TV dzi\u0119ki tej integracji. Wy\u0142\u0105cza si\u0119 tylko sam odtwarzacz multimedialny w Home Assistant!", + "title": "Potwierdzenie dodania Apple TV" + }, + "pair_no_pin": { + "description": "Parowanie jest wymagane dla us\u0142ugi \"{protocol}\". Aby kontynuowa\u0107, wprowad\u017a kod {pin} na swoim Apple TV.", + "title": "Parowanie" + }, + "pair_with_pin": { + "data": { + "pin": "Kod PIN" + }, + "description": "Parowanie jest wymagane dla protoko\u0142u \"{protocol}\". Wprowad\u017a kod PIN wy\u015bwietlony na ekranie. Zera poprzedzaj\u0105ce nale\u017cy pomin\u0105\u0107, tj. wpisa\u0107 123, zamiast 0123.", + "title": "Parowanie" + }, + "reconfigure": { + "description": "Ten Apple TV ma pewne problemy z po\u0142\u0105czeniem i musi zosta\u0107 ponownie skonfigurowany.", + "title": "Ponowna konfiguracja urz\u0105dzenia" + }, + "service_problem": { + "description": "Wyst\u0105pi\u0142 problem podczas parowania protoko\u0142u \"{protocol}\". Zostanie on zignorowany.", + "title": "Nie uda\u0142o si\u0119 doda\u0107 us\u0142ugi" + }, + "user": { + "data": { + "device_input": "Urz\u0105dzenie" + }, + "description": "Zacznij od wprowadzenia nazwy urz\u0105dzenia (np. Kuchnia lub Sypialnia) lub adresu IP Apple TV, kt\u00f3re chcesz doda\u0107. Je\u015bli jakie\u015b urz\u0105dzenia zosta\u0142y automatycznie znalezione w Twojej sieci, s\u0105 one pokazane poni\u017cej. \n\nJe\u015bli nie widzisz swojego urz\u0105dzenia lub wyst\u0119puj\u0105 jakiekolwiek problemy, spr\u00f3buj okre\u015bli\u0107 adres IP urz\u0105dzenia. \n\n{devices}", + "title": "Konfiguracja nowego Apple TV" + } } - } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nie w\u0142\u0105czaj urz\u0105dzenia podczas uruchamiania Home Assistanta" + }, + "description": "Skonfiguruj og\u00f3lne ustawienia urz\u0105dzenia" + } + } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index 2fbdd53c07..e3f5804ceb 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -3,6 +3,8 @@ "abort": { "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "backoff": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 (\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0412\u044b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434), \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "device_did_not_pair": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u044b\u0442\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." @@ -17,6 +19,7 @@ "flow_title": "Apple TV: {name}", "step": { "confirm": { + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Apple TV `{name}` \u0432 Home Assistant. \n\n**\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0412\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e PIN-\u043a\u043e\u0434\u043e\u0432.** \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0412\u044b *\u043d\u0435* \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c Apple TV \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0412 Home Assistant \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440!", "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 Apple TV" }, "pair_no_pin": { @@ -26,20 +29,23 @@ "pair_with_pin": { "data": { "pin": "PIN-\u043a\u043e\u0434" - } + }, + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435. \u041f\u0435\u0440\u0432\u044b\u0435 \u043d\u0443\u043b\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u043f\u0443\u0449\u0435\u043d\u044b, \u0442.\u0435. \u0432\u0432\u0435\u0434\u0438\u0442\u0435 123, \u0435\u0441\u043b\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u0434 0123.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" }, "reconfigure": { - "description": "\u042d\u0442\u043e\u0442 Apple TV \u0438\u0441\u043f\u044b\u0442\u044b\u0432\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0442\u0440\u0443\u0434\u043d\u043e\u0441\u0442\u0438 \u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c, \u0438 \u0435\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", + "description": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Apple TV \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438, \u0435\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "service_problem": { + "description": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u042d\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443" }, "user": { "data": { "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, - "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438, \u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0438\u0436\u0435. \n\n \u0415\u0441\u043b\u0438 \u0432\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0438\u0441\u043f\u044b\u0442\u044b\u0432\u0430\u0435\u0442\u0435 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \n\n {devices}", + "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438, \u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0438\u0436\u0435. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0434\u0440\u0443\u0433\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \n\n {devices}", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" } } diff --git a/homeassistant/components/bsblan/translations/it.json b/homeassistant/components/bsblan/translations/it.json index 1f27531f76..3eb7feec61 100644 --- a/homeassistant/components/bsblan/translations/it.json +++ b/homeassistant/components/bsblan/translations/it.json @@ -12,7 +12,9 @@ "data": { "host": "Host", "passkey": "Stringa passkey", - "port": "Porta" + "password": "Password", + "port": "Porta", + "username": "Nome utente" }, "description": "Configura il tuo dispositivo BSB-Lan per l'integrazione con Home Assistant.", "title": "Collegamento al dispositivo BSB-Lan" diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json new file mode 100644 index 0000000000..0510da2305 --- /dev/null +++ b/homeassistant/components/hyperion/translations/it.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "auth_new_token_not_granted_error": "Il token appena creato non \u00e8 stato approvato sull'interfaccia utente di Hyperion", + "auth_new_token_not_work_error": "Autenticazione utilizzando il token appena creato non riuscita", + "auth_required_error": "Impossibile determinare se \u00e8 necessaria l'autorizzazione", + "cannot_connect": "Impossibile connettersi", + "no_id": "L'istanza Hyperion Ambilight non ha segnalato il suo ID" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_access_token": "Token di accesso non valido" + }, + "step": { + "auth": { + "data": { + "create_token": "Crea automaticamente un nuovo token", + "token": "Oppure fornisci un token preesistente" + }, + "description": "Configura l'autorizzazione per il tuo server Hyperion Ambilight" + }, + "confirm": { + "description": "Vuoi aggiungere questo Hyperion Ambilight a Home Assistant? \n\n ** Host:** {host}\n ** Porta:** {port}\n ** ID:** {id}", + "title": "Conferma l'aggiunta del servizio Hyperion Ambilight" + }, + "create_token": { + "description": "Scegli **Invia** di seguito per richiedere un nuovo token di autenticazione. Verrai reindirizzato all'interfaccia utente di Hyperion per approvare la richiesta. Verifica che l'ID visualizzato sia \"{auth_id}\"", + "title": "Crea automaticamente un nuovo token di autenticazione" + }, + "create_token_external": { + "title": "Accetta il nuovo token nell'interfaccia utente di Hyperion" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Priorit\u00e0 Hyperion da usare per colori ed effetti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/it.json b/homeassistant/components/ipma/translations/it.json index 467cc64f56..4dd8ddda76 100644 --- a/homeassistant/components/ipma/translations/it.json +++ b/homeassistant/components/ipma/translations/it.json @@ -15,5 +15,10 @@ "title": "Posizione" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint API IPMA raggiungibile" + } } } \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/es.json b/homeassistant/components/kulersky/translations/es.json new file mode 100644 index 0000000000..520df7ee4c --- /dev/null +++ b/homeassistant/components/kulersky/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/it.json b/homeassistant/components/kulersky/translations/it.json new file mode 100644 index 0000000000..0278fe07bf --- /dev/null +++ b/homeassistant/components/kulersky/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/pl.json b/homeassistant/components/kulersky/translations/pl.json new file mode 100644 index 0000000000..a8ee3fa57a --- /dev/null +++ b/homeassistant/components/kulersky/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/cs.json b/homeassistant/components/lovelace/translations/cs.json index f946a859ea..d08fcdc7fe 100644 --- a/homeassistant/components/lovelace/translations/cs.json +++ b/homeassistant/components/lovelace/translations/cs.json @@ -1,7 +1,7 @@ { "system_health": { "info": { - "dashboards": "Dashboardy", + "dashboards": "Ovl\u00e1dac\u00ed panely", "mode": "Re\u017eim", "resources": "Zdroje", "views": "Pohledy" diff --git a/homeassistant/components/mobile_app/translations/it.json b/homeassistant/components/mobile_app/translations/it.json index 8ff6dfb982..f5ba52b1a5 100644 --- a/homeassistant/components/mobile_app/translations/it.json +++ b/homeassistant/components/mobile_app/translations/it.json @@ -8,5 +8,10 @@ "description": "Si desidera configurare il componente App per dispositivi mobili?" } } + }, + "device_automation": { + "action_type": { + "notify": "Invia una notifica" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 00e949b652..4db72851aa 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -36,5 +36,13 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Movimento rilevato", + "camera_person": "Persona rilevata", + "camera_sound": "Suono rilevato", + "doorbell_chime": "Campanello premuto" + } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/cs.json b/homeassistant/components/ozw/translations/cs.json index b9cc99e72b..4ba465a314 100644 --- a/homeassistant/components/ozw/translations/cs.json +++ b/homeassistant/components/ozw/translations/cs.json @@ -13,6 +13,9 @@ "addon_start_failed": "Spu\u0161t\u011bn\u00ed dopl\u0148ku OpenZWave se nezda\u0159ilo. Zkontrolujte konfiguraci." }, "step": { + "hassio_confirm": { + "title": "Nastaven\u00ed integrace OpenZWave s dopl\u0148kem OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "Pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor" diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json index 60d64f9afb..f06c2896bc 100644 --- a/homeassistant/components/ozw/translations/es.json +++ b/homeassistant/components/ozw/translations/es.json @@ -4,6 +4,8 @@ "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento de OpenZWave.", "addon_install_failed": "No se pudo instalar el complemento de OpenZWave.", "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de OpenZWave.", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "mqtt_required": "La integraci\u00f3n de MQTT no est\u00e1 configurada", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, @@ -14,6 +16,9 @@ "install_addon": "Espera mientras finaliza la instalaci\u00f3n del complemento OpenZWave. Esto puede tardar varios minutos." }, "step": { + "hassio_confirm": { + "title": "Configurar la integraci\u00f3n de OpenZWave con el complemento OpenZWave" + }, "install_addon": { "title": "La instalaci\u00f3n del complemento OpenZWave se ha iniciado" }, diff --git a/homeassistant/components/ozw/translations/it.json b/homeassistant/components/ozw/translations/it.json index e03c71ae70..ff3e0a711c 100644 --- a/homeassistant/components/ozw/translations/it.json +++ b/homeassistant/components/ozw/translations/it.json @@ -4,13 +4,24 @@ "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo OpenZWave.", "addon_install_failed": "Impossibile installare il componente aggiuntivo OpenZWave.", "addon_set_config_failed": "Impossibile impostare la configurazione di OpenZWave.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "mqtt_required": "L'integrazione MQTT non \u00e8 impostata", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { "addon_start_failed": "Impossibile avviare il componente aggiuntivo OpenZWave. Controlla la configurazione." }, + "progress": { + "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo OpenZWave. Questa operazione pu\u00f2 richiedere diversi minuti." + }, "step": { + "hassio_confirm": { + "title": "Configura l'integrazione di OpenZWave con il componente aggiuntivo OpenZWave" + }, + "install_addon": { + "title": "L'installazione del componente aggiuntivo OpenZWave \u00e8 iniziata" + }, "on_supervisor": { "data": { "use_addon": "Usa il componente aggiuntivo OpenZWave Supervisor" diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json index a143163ca1..c9fd17c59b 100644 --- a/homeassistant/components/ozw/translations/pl.json +++ b/homeassistant/components/ozw/translations/pl.json @@ -4,6 +4,8 @@ "addon_info_failed": "Nie uda\u0142o si\u0119 pobra\u0107 informacji o dodatku OpenZWave", "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku OpenZWave", "addon_set_config_failed": "Nie uda\u0142o si\u0119 ustawi\u0107 konfiguracji OpenZWave", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, @@ -14,6 +16,9 @@ "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku OpenZWave. Mo\u017ce to potrwa\u0107 kilka minut." }, "step": { + "hassio_confirm": { + "title": "Konfiguracja integracji OpenZWave z dodatkiem OpenZWave" + }, "install_addon": { "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku OpenZWave" }, diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json index 129043728a..07dc84eae0 100644 --- a/homeassistant/components/ozw/translations/ru.json +++ b/homeassistant/components/ozw/translations/ru.json @@ -4,7 +4,7 @@ "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 OpenZWave.", "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c OpenZWave.", "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e OpenZWave.", - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." @@ -17,7 +17,7 @@ }, "step": { "hassio_confirm": { - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 OpenZWave \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c OpenZWave add-on." + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" }, "install_addon": { "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" From c4426a73b397a2256940e4c9689a8bc5ca6318ef Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Fri, 4 Dec 2020 18:40:56 -0600 Subject: [PATCH 026/302] Remove zerproc threaded upstream reconnect logic (#43910) --- homeassistant/components/zerproc/light.py | 4 +- .../components/zerproc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zerproc/test_light.py | 70 ++++++++++++------- 5 files changed, 50 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 2ab4bc127c..b45ca4497a 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -36,7 +36,7 @@ def connect_lights(lights: List[pyzerproc.Light]) -> List[pyzerproc.Light]: connected = [] for light in lights: try: - light.connect(auto_reconnect=True) + light.connect() connected.append(light) except pyzerproc.ZerprocException: _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) @@ -193,6 +193,8 @@ class ZerprocLight(LightEntity): def update(self): """Fetch new state data for this light.""" try: + if not self._light.connected: + self._light.connect() state = self._light.get_state() except pyzerproc.ZerprocException: if self._available: diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index 4f9b559bc1..344ea56910 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": [ - "pyzerproc==0.2.5" + "pyzerproc==0.3.0" ], "codeowners": [ "@emlove" diff --git a/requirements_all.txt b/requirements_all.txt index 1148222aaf..da06aa3cd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1903,7 +1903,7 @@ pyxeoma==1.4.1 pyzbar==0.1.7 # homeassistant.components.zerproc -pyzerproc==0.2.5 +pyzerproc==0.3.0 # homeassistant.components.qnap qnapstats==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e451e137dc..36c65ae0ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -932,7 +932,7 @@ pywebpush==1.9.2 pywilight==0.0.65 # homeassistant.components.zerproc -pyzerproc==0.2.5 +pyzerproc==0.3.0 # homeassistant.components.rachio rachiopy==1.0.3 diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 871f91d447..14756f1183 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -24,19 +24,27 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import patch +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -async def mock_light(hass): +async def mock_entry(hass): + """Create a mock light entity.""" + return MockConfigEntry(domain=DOMAIN) + + +@pytest.fixture +async def mock_light(hass, mock_entry): """Create a mock light entity.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) - light = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + light = MagicMock(spec=pyzerproc.Light) + light.address = "AA:BB:CC:DD:EE:FF" + light.name = "LEDBlue-CCDDEEFF" + light.connected = False mock_state = pyzerproc.LightState(False, (0, 0, 0)) @@ -49,31 +57,36 @@ async def mock_light(hass): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() + light.connected = True + return light -async def test_init(hass): +async def test_init(hass, mock_entry): """Test platform setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) - mock_light_1 = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") - mock_light_2 = pyzerproc.Light("11:22:33:44:55:66", "LEDBlue-33445566") + mock_light_1 = MagicMock(spec=pyzerproc.Light) + mock_light_1.address = "AA:BB:CC:DD:EE:FF" + mock_light_1.name = "LEDBlue-CCDDEEFF" + mock_light_1.connected = True + + mock_light_2 = MagicMock(spec=pyzerproc.Light) + mock_light_2.address = "11:22:33:44:55:66" + mock_light_2.name = "LEDBlue-33445566" + mock_light_2.connected = True mock_state_1 = pyzerproc.LightState(False, (0, 0, 0)) mock_state_2 = pyzerproc.LightState(True, (0, 80, 255)) + mock_light_1.get_state.return_value = mock_state_1 + mock_light_2.get_state.return_value = mock_state_2 + with patch( "homeassistant.components.zerproc.light.pyzerproc.discover", return_value=[mock_light_1, mock_light_2], - ), patch.object(mock_light_1, "connect"), patch.object( - mock_light_2, "connect" - ), patch.object( - mock_light_1, "get_state", return_value=mock_state_1 - ), patch.object( - mock_light_2, "get_state", return_value=mock_state_2 ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -98,22 +111,17 @@ async def test_init(hass): ATTR_XY_COLOR: (0.138, 0.08), } - with patch.object(hass.loop, "stop"), patch.object( - mock_light_1, "disconnect" - ) as mock_disconnect_1, patch.object( - mock_light_2, "disconnect" - ) as mock_disconnect_2: + with patch.object(hass.loop, "stop"): await hass.async_stop() - assert mock_disconnect_1.called - assert mock_disconnect_2.called + assert mock_light_1.disconnect.called + assert mock_light_2.disconnect.called -async def test_discovery_exception(hass): +async def test_discovery_exception(hass, mock_entry): """Test platform setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) with patch( @@ -127,14 +135,16 @@ async def test_discovery_exception(hass): assert len(hass.data[DOMAIN]["addresses"]) == 0 -async def test_connect_exception(hass): +async def test_connect_exception(hass, mock_entry): """Test platform setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) - mock_light = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + mock_light = MagicMock(spec=pyzerproc.Light) + mock_light.address = "AA:BB:CC:DD:EE:FF" + mock_light.name = "LEDBlue-CCDDEEFF" + mock_light.connected = False with patch( "homeassistant.components.zerproc.light.pyzerproc.discover", @@ -149,6 +159,14 @@ async def test_connect_exception(hass): assert len(hass.data[DOMAIN]["addresses"]) == 0 +async def test_remove_entry(hass, mock_light, mock_entry): + """Test platform setup.""" + with patch.object(mock_light, "disconnect") as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + async def test_light_turn_on(hass, mock_light): """Test ZerprocLight turn_on.""" utcnow = dt_util.utcnow() From 0670124e8da343787acb2e47852e2f93bfb353d0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 5 Dec 2020 01:55:19 -0800 Subject: [PATCH 027/302] Address PR cleanup for nest device triggers (#43961) --- homeassistant/components/nest/device_trigger.py | 2 +- tests/components/nest/test_device_trigger.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 199dcf425d..e5bd7ea1ca 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -54,7 +54,7 @@ async def async_get_device_trigger_types( # Determine the set of event types based on the supported device traits trigger_types = [] - for trait in nest_device.traits.keys(): + for trait in nest_device.traits: trigger_type = DEVICE_TRAIT_TRIGGER_MAP.get(trait) if trigger_type: trigger_types.append(trigger_type) diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 89dccf6c31..3199b89f21 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -148,21 +148,21 @@ async def test_multiple_devices(hass): triggers = await async_get_device_automations(hass, "trigger", entry1.device_id) assert len(triggers) == 1 - assert { + assert triggers[0] == { "platform": "device", "domain": DOMAIN, "type": "camera_sound", "device_id": entry1.device_id, - } == triggers[0] + } triggers = await async_get_device_automations(hass, "trigger", entry2.device_id) assert len(triggers) == 1 - assert { + assert triggers[0] == { "platform": "device", "domain": DOMAIN, "type": "doorbell_chime", "device_id": entry2.device_id, - } == triggers[0] + } async def test_triggers_for_invalid_device_id(hass): @@ -205,7 +205,7 @@ async def test_no_triggers(hass): assert entry.unique_id == "some-device-id-camera" triggers = await async_get_device_automations(hass, "trigger", entry.device_id) - assert [] == triggers + assert triggers == [] async def test_fires_on_camera_motion(hass, calls): From c5adaa1f5cea5b26766386f97064b1be9501b590 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 5 Dec 2020 05:32:49 -0500 Subject: [PATCH 028/302] Return unique id of Blink binary sensor (#43942) --- homeassistant/components/blink/binary_sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 1841dbbc43..f69c94f0f5 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -44,6 +44,11 @@ class BlinkBinarySensor(BinarySensorEntity): """Return the name of the blink sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + @property def device_class(self): """Return the class of this device.""" From bc83e307614b0263dd772c9eb2ff7c4117cc0d6b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Dec 2020 11:53:43 +0100 Subject: [PATCH 029/302] Fix device refresh service can always add devices (#43950) --- homeassistant/components/deconz/gateway.py | 6 ++++-- homeassistant/components/deconz/services.py | 8 +++---- tests/components/deconz/test_binary_sensor.py | 21 ++++++++++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index dc41bb778e..881ea883c4 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -121,9 +121,11 @@ class DeconzGateway: async_dispatcher_send(self.hass, self.signal_reachable, True) @callback - def async_add_device_callback(self, device_type, device=None) -> None: + def async_add_device_callback( + self, device_type, device=None, force: bool = False + ) -> None: """Handle event of new device creation in deCONZ.""" - if not self.option_allow_new_devices: + if not force and not self.option_allow_new_devices: return args = [] diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 2c286fac0a..d524354ff0 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -146,10 +146,10 @@ async def async_refresh_devices_service(hass, data): await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP) - gateway.async_add_device_callback(NEW_LIGHT) - gateway.async_add_device_callback(NEW_SCENE) - gateway.async_add_device_callback(NEW_SENSOR) + gateway.async_add_device_callback(NEW_GROUP, force=True) + gateway.async_add_device_callback(NEW_LIGHT, force=True) + gateway.async_add_device_callback(NEW_SCENE, force=True) + gateway.async_add_device_callback(NEW_SENSOR, force=True) async def async_remove_orphaned_entries_service(hass, data): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 5038c5bf3f..78a4f1e937 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -10,15 +10,19 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, + CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SENSORS = { "1": { "id": "Presence sensor id", @@ -172,7 +176,7 @@ async def test_add_new_binary_sensor_ignored(hass): """Test that adding a new binary sensor is not allowed.""" config_entry = await setup_deconz_integration( hass, - options={CONF_ALLOW_NEW_DEVICES: False}, + options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 @@ -188,8 +192,23 @@ async def test_add_new_binary_sensor_ignored(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + assert not hass.states.get("binary_sensor.presence_sensor") entity_registry = await hass.helpers.entity_registry.async_get_registry() assert ( len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 ) + + with patch( + "pydeconz.DeconzSession.request", + return_value={ + "groups": {}, + "lights": {}, + "sensors": {"1": deepcopy(SENSORS["1"])}, + }, + ): + await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("binary_sensor.presence_sensor") From 378424b2c496936da9f7b601ea58bbfef1eec493 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 5 Dec 2020 12:57:49 +0100 Subject: [PATCH 030/302] Refactor LCN integration (#40665) * Moved configuration schemes to schemes.py * Renamed LcnDevice to LcnEntity. Renamed address_connection to device_connection. * Rename schemes.py to schemas.py --- homeassistant/components/lcn/__init__.py | 180 +---------------- homeassistant/components/lcn/binary_sensor.py | 26 +-- homeassistant/components/lcn/climate.py | 20 +- homeassistant/components/lcn/cover.py | 32 +-- homeassistant/components/lcn/helpers.py | 18 +- homeassistant/components/lcn/light.py | 26 +-- homeassistant/components/lcn/scene.py | 10 +- homeassistant/components/lcn/schemas.py | 190 ++++++++++++++++++ homeassistant/components/lcn/sensor.py | 24 +-- homeassistant/components/lcn/switch.py | 26 +-- 10 files changed, 286 insertions(+), 266 deletions(-) create mode 100644 homeassistant/components/lcn/schemas.py diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index faba23a52b..fa18c0e578 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -2,11 +2,8 @@ import logging import pypck -import voluptuous as vol -from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( - CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COVERS, CONF_HOST, @@ -16,52 +13,21 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SWITCHES, - CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from .const import ( - BINSENSOR_PORTS, CONF_CLIMATES, CONF_CONNECTIONS, CONF_DIM_MODE, - CONF_DIMMABLE, - CONF_LOCKABLE, - CONF_MAX_TEMP, - CONF_MIN_TEMP, - CONF_MOTOR, - CONF_OUTPUT, - CONF_OUTPUTS, - CONF_REGISTER, - CONF_REVERSE_TIME, - CONF_SCENE, CONF_SCENES, - CONF_SETPOINT, CONF_SK_NUM_TRIES, - CONF_SOURCE, - CONF_TRANSITION, DATA_LCN, - DIM_MODES, DOMAIN, - KEYS, - LED_PORTS, - LOGICOP_PORTS, - MOTOR_PORTS, - MOTOR_REVERSE_TIME, - OUTPUT_PORTS, - RELAY_PORTS, - S0_INPUTS, - SETPOINTS, - THRESHOLDS, - VAR_UNITS, - VARIABLES, ) -from .helpers import has_unique_connection_names, is_address +from .schemas import CONFIG_SCHEMA # noqa: 401 from .services import ( DynText, Led, @@ -80,141 +46,6 @@ from .services import ( _LOGGER = logging.getLogger(__name__) -BINARY_SENSORS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_SOURCE): vol.All( - vol.Upper, vol.In(SETPOINTS + KEYS + BINSENSOR_PORTS) - ), - } -) - -CLIMATES_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_SOURCE): vol.All(vol.Upper, vol.In(VARIABLES)), - vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS)), - vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_LOCKABLE, default=False): vol.Coerce(bool), - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): vol.In( - TEMP_CELSIUS, TEMP_FAHRENHEIT - ), - } -) - -COVERS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)), - vol.Optional(CONF_REVERSE_TIME): vol.All(vol.Upper, vol.In(MOTOR_REVERSE_TIME)), - } -) - -LIGHTS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All( - vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS) - ), - vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), - vol.Optional(CONF_TRANSITION, default=0): vol.All( - vol.Coerce(float), vol.Range(min=0.0, max=486.0), lambda value: value * 1000 - ), - } -) - -SCENES_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_REGISTER): vol.All(vol.Coerce(int), vol.Range(0, 9)), - vol.Required(CONF_SCENE): vol.All(vol.Coerce(int), vol.Range(0, 9)), - vol.Optional(CONF_OUTPUTS): vol.All( - cv.ensure_list, [vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS))] - ), - vol.Optional(CONF_TRANSITION, default=None): vol.Any( - vol.All( - vol.Coerce(int), - vol.Range(min=0.0, max=486.0), - lambda value: value * 1000, - ), - None, - ), - } -) - -SENSORS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_SOURCE): vol.All( - vol.Upper, - vol.In( - VARIABLES - + SETPOINTS - + THRESHOLDS - + S0_INPUTS - + LED_PORTS - + LOGICOP_PORTS - ), - ), - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( - vol.Upper, vol.In(VAR_UNITS) - ), - } -) - -SWITCHES_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All( - vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS) - ), - } -) - -CONNECTION_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SK_NUM_TRIES, default=0): cv.positive_int, - vol.Optional(CONF_DIM_MODE, default="steps50"): vol.All( - vol.Upper, vol.In(DIM_MODES) - ), - vol.Optional(CONF_NAME): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CONNECTIONS): vol.All( - cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA] - ), - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [BINARY_SENSORS_SCHEMA] - ), - vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATES_SCHEMA]), - vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), - vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), - vol.Optional(CONF_SCENES): vol.All(cv.ensure_list, [SCENES_SCHEMA]), - vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSORS_SCHEMA]), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - async def async_setup(hass, config): """Set up the LCN component.""" @@ -292,13 +123,13 @@ async def async_setup(hass, config): return True -class LcnDevice(Entity): +class LcnEntity(Entity): """Parent class for all devices associated with the LCN component.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN device.""" self.config = config - self.address_connection = address_connection + self.device_connection = device_connection self._name = config[CONF_NAME] @property @@ -308,7 +139,7 @@ class LcnDevice(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - self.address_connection.register_for_inputs(self.input_received) + self.device_connection.register_for_inputs(self.input_received) @property def name(self): @@ -317,4 +148,3 @@ class LcnDevice(Entity): def input_received(self, input_obj): """Set state/value when LCN input object (command) is received.""" - raise NotImplementedError("Pure virtual function.") diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 7b4cedfeba..5d712045c9 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -4,7 +4,7 @@ import pypck from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import BINSENSOR_PORTS, CONF_CONNECTIONS, CONF_SOURCE, DATA_LCN, SETPOINTS from .helpers import get_connection @@ -36,12 +36,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): +class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for regulator locks.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN binary sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.setpoint_variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] @@ -50,7 +50,7 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( self.setpoint_variable ) @@ -71,12 +71,12 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): self.async_write_ha_state() -class LcnBinarySensor(LcnDevice, BinarySensorEntity): +class LcnBinarySensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for binary sensor ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN binary sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.bin_sensor_port = pypck.lcn_defs.BinSensorPort[config[CONF_SOURCE]] @@ -85,7 +85,7 @@ class LcnBinarySensor(LcnDevice, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( self.bin_sensor_port ) @@ -103,12 +103,12 @@ class LcnBinarySensor(LcnDevice, BinarySensorEntity): self.async_write_ha_state() -class LcnLockKeysSensor(LcnDevice, BinarySensorEntity): +class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN sensor for key locks.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.source = pypck.lcn_defs.Key[config[CONF_SOURCE]] self._value = None @@ -116,7 +116,7 @@ class LcnLockKeysSensor(LcnDevice, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 8b0f4951bf..e3eb92a426 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -5,7 +5,7 @@ import pypck from homeassistant.components.climate import ClimateEntity, const from homeassistant.const import ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_LOCKABLE, @@ -40,12 +40,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnClimate(LcnDevice, ClimateEntity): +class LcnClimate(LcnEntity, ClimateEntity): """Representation of a LCN climate device.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize of a LCN climate device.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] self.setpoint = pypck.lcn_defs.Var[config[CONF_SETPOINT]] @@ -63,8 +63,8 @@ class LcnClimate(LcnDevice, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.variable) - await self.address_connection.activate_status_request_handler(self.setpoint) + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): @@ -120,16 +120,14 @@ class LcnClimate(LcnDevice, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == const.HVAC_MODE_HEAT: - if not await self.address_connection.lock_regulator( + if not await self.device_connection.lock_regulator( self.regulator_id, False ): return self._is_on = True self.async_write_ha_state() elif hvac_mode == const.HVAC_MODE_OFF: - if not await self.address_connection.lock_regulator( - self.regulator_id, True - ): + if not await self.device_connection.lock_regulator(self.regulator_id, True): return self._is_on = False self._target_temperature = None @@ -141,7 +139,7 @@ class LcnClimate(LcnDevice, ClimateEntity): if temperature is None: return - if not await self.address_connection.var_abs( + if not await self.device_connection.var_abs( self.setpoint, temperature, self.unit ): return diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index ae88441f89..c5e407573b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -4,7 +4,7 @@ import pypck from homeassistant.components.cover import CoverEntity from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import CONF_CONNECTIONS, CONF_MOTOR, CONF_REVERSE_TIME, DATA_LCN from .helpers import get_connection @@ -34,12 +34,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputsCover(LcnDevice, CoverEntity): +class LcnOutputsCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to output ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN cover.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output_ids = [ pypck.lcn_defs.OutputPort["OUTPUTUP"].value, @@ -59,10 +59,10 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( pypck.lcn_defs.OutputPort["OUTPUTUP"] ) - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( pypck.lcn_defs.OutputPort["OUTPUTDOWN"] ) @@ -89,7 +89,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_close_cover(self, **kwargs): """Close the cover.""" state = pypck.lcn_defs.MotorStateModifier.DOWN - if not await self.address_connection.control_motors_outputs( + if not await self.device_connection.control_motors_outputs( state, self.reverse_time ): return @@ -100,7 +100,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_open_cover(self, **kwargs): """Open the cover.""" state = pypck.lcn_defs.MotorStateModifier.UP - if not await self.address_connection.control_motors_outputs( + if not await self.device_connection.control_motors_outputs( state, self.reverse_time ): return @@ -112,7 +112,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the cover.""" state = pypck.lcn_defs.MotorStateModifier.STOP - if not await self.address_connection.control_motors_outputs(state): + if not await self.device_connection.control_motors_outputs(state): return self._is_closing = False self._is_opening = False @@ -143,12 +143,12 @@ class LcnOutputsCover(LcnDevice, CoverEntity): self.async_write_ha_state() -class LcnRelayCover(LcnDevice, CoverEntity): +class LcnRelayCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to relays.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN cover.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.motor = pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] self.motor_port_onoff = self.motor.value * 2 @@ -161,7 +161,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.motor) + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): @@ -187,7 +187,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): """Close the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN - if not await self.address_connection.control_motors_relays(states): + if not await self.device_connection.control_motors_relays(states): return self._is_opening = False self._is_closing = True @@ -197,7 +197,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): """Open the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP - if not await self.address_connection.control_motors_relays(states): + if not await self.device_connection.control_motors_relays(states): return self._is_closed = False self._is_opening = True @@ -208,7 +208,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): """Stop the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP - if not await self.address_connection.control_motors_relays(states): + if not await self.device_connection.control_motors_relays(states): return self._is_closing = False self._is_opening = False diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index f4545817c9..18342aa1d9 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -26,23 +26,25 @@ def get_connection(connections, connection_id=None): return connection -def has_unique_connection_names(connections): +def has_unique_host_names(hosts): """Validate that all connection names are unique. Use 'pchk' as default connection_name (or add a numeric suffix if pchk' is already in use. """ - for suffix, connection in enumerate(connections): - connection_name = connection.get(CONF_NAME) - if connection_name is None: + suffix = 0 + for host in hosts: + host_name = host.get(CONF_NAME) + if host_name is None: if suffix == 0: - connection[CONF_NAME] = DEFAULT_NAME + host[CONF_NAME] = DEFAULT_NAME else: - connection[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" + host[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" + suffix += 1 schema = vol.Schema(vol.Unique()) - schema([connection.get(CONF_NAME) for connection in connections]) - return connections + schema([host.get(CONF_NAME) for host in hosts]) + return hosts def is_address(value): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index def025e0cf..c6ef895b7d 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_DIMMABLE, @@ -49,12 +49,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputLight(LcnDevice, LightEntity): +class LcnOutputLight(LcnEntity, LightEntity): """Representation of a LCN light for output ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN light.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] @@ -68,7 +68,7 @@ class LcnOutputLight(LcnDevice, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -100,7 +100,7 @@ class LcnOutputLight(LcnDevice, LightEntity): else: transition = self._transition - if not await self.address_connection.dim_output( + if not await self.device_connection.dim_output( self.output.value, percent, transition ): return @@ -117,7 +117,7 @@ class LcnOutputLight(LcnDevice, LightEntity): else: transition = self._transition - if not await self.address_connection.dim_output( + if not await self.device_connection.dim_output( self.output.value, 0, transition ): return @@ -141,12 +141,12 @@ class LcnOutputLight(LcnDevice, LightEntity): self.async_write_ha_state() -class LcnRelayLight(LcnDevice, LightEntity): +class LcnRelayLight(LcnEntity, LightEntity): """Representation of a LCN light for relay ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN light.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] @@ -155,7 +155,7 @@ class LcnRelayLight(LcnDevice, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -167,7 +167,7 @@ class LcnRelayLight(LcnDevice, LightEntity): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = True self.async_write_ha_state() @@ -177,7 +177,7 @@ class LcnRelayLight(LcnDevice, LightEntity): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index cac13ee165..ed211473e2 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -6,7 +6,7 @@ import pypck from homeassistant.components.scene import Scene from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_OUTPUTS, @@ -41,12 +41,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnScene(LcnDevice, Scene): +class LcnScene(LcnEntity, Scene): """Representation of a LCN scene.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN scene.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.register_id = config[CONF_REGISTER] self.scene_id = config[CONF_SCENE] @@ -69,7 +69,7 @@ class LcnScene(LcnDevice, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate scene.""" - await self.address_connection.activate_scene( + await self.device_connection.activate_scene( self.register_id, self.scene_id, self.output_ports, diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py new file mode 100644 index 0000000000..1cc51f400d --- /dev/null +++ b/homeassistant/components/lcn/schemas.py @@ -0,0 +1,190 @@ +"""Schema definitions for LCN configuration and websockets api.""" +import voluptuous as vol + +from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP +from homeassistant.const import ( + CONF_ADDRESS, + CONF_BINARY_SENSORS, + CONF_COVERS, + CONF_HOST, + CONF_LIGHTS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + BINSENSOR_PORTS, + CONF_CLIMATES, + CONF_CONNECTIONS, + CONF_DIM_MODE, + CONF_DIMMABLE, + CONF_LOCKABLE, + CONF_MAX_TEMP, + CONF_MIN_TEMP, + CONF_MOTOR, + CONF_OUTPUT, + CONF_OUTPUTS, + CONF_REGISTER, + CONF_REVERSE_TIME, + CONF_SCENE, + CONF_SCENES, + CONF_SETPOINT, + CONF_SK_NUM_TRIES, + CONF_SOURCE, + CONF_TRANSITION, + DIM_MODES, + DOMAIN, + KEYS, + LED_PORTS, + LOGICOP_PORTS, + MOTOR_PORTS, + MOTOR_REVERSE_TIME, + OUTPUT_PORTS, + RELAY_PORTS, + S0_INPUTS, + SETPOINTS, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + THRESHOLDS, + VAR_UNITS, + VARIABLES, +) +from .helpers import has_unique_host_names, is_address + +# +# Domain data +# + +DOMAIN_DATA_BINARY_SENSOR = { + vol.Required(CONF_SOURCE): vol.All( + vol.Upper, vol.In(SETPOINTS + KEYS + BINSENSOR_PORTS) + ), +} + + +DOMAIN_DATA_CLIMATE = { + vol.Required(CONF_SOURCE): vol.All(vol.Upper, vol.In(VARIABLES)), + vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS)), + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_LOCKABLE, default=False): vol.Coerce(bool), + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): vol.In( + TEMP_CELSIUS, TEMP_FAHRENHEIT + ), +} + + +DOMAIN_DATA_COVER = { + vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)), + vol.Optional(CONF_REVERSE_TIME, default="rt1200"): vol.All( + vol.Upper, vol.In(MOTOR_REVERSE_TIME) + ), +} + + +DOMAIN_DATA_LIGHT = { + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), + vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), + vol.Optional(CONF_TRANSITION, default=0): vol.All( + vol.Coerce(float), vol.Range(min=0.0, max=486.0), lambda value: value * 1000 + ), +} + + +DOMAIN_DATA_SCENE = { + vol.Required(CONF_REGISTER): vol.All(vol.Coerce(int), vol.Range(0, 9)), + vol.Required(CONF_SCENE): vol.All(vol.Coerce(int), vol.Range(0, 9)), + vol.Optional(CONF_OUTPUTS, default=[]): vol.All( + cv.ensure_list, [vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS))] + ), + vol.Optional(CONF_TRANSITION, default=None): vol.Any( + vol.All( + vol.Coerce(int), + vol.Range(min=0.0, max=486.0), + lambda value: value * 1000, + ), + None, + ), +} + +DOMAIN_DATA_SENSOR = { + vol.Required(CONF_SOURCE): vol.All( + vol.Upper, + vol.In( + VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS + LED_PORTS + LOGICOP_PORTS + ), + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( + vol.Upper, vol.In(VAR_UNITS) + ), +} + + +DOMAIN_DATA_SWITCH = { + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), +} + +# +# Configuration +# + +DOMAIN_DATA_BASE = { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, +} + +BINARY_SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_BINARY_SENSOR}) + +CLIMATES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_CLIMATE}) + +COVERS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_COVER}) + +LIGHTS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_LIGHT}) + +SCENES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SCENE}) + +SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SENSOR}) + +SWITCHES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SWITCH}) + +CONNECTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SK_NUM_TRIES, default=0): cv.positive_int, + vol.Optional(CONF_DIM_MODE, default="steps50"): vol.All( + vol.Upper, vol.In(DIM_MODES) + ), + vol.Optional(CONF_NAME): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CONNECTIONS): vol.All( + cv.ensure_list, has_unique_host_names, [CONNECTION_SCHEMA] + ), + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSORS_SCHEMA] + ), + vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATES_SCHEMA]), + vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SCENES): vol.All(cv.ensure_list, [SCENES_SCHEMA]), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSORS_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index ddf7e61a3f..26b54def97 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -3,7 +3,7 @@ import pypck from homeassistant.const import CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_SOURCE, @@ -30,24 +30,24 @@ async def async_setup_platform( addr = pypck.lcn_addr.LcnAddr(*address) connections = hass.data[DATA_LCN][CONF_CONNECTIONS] connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + device_connection = connection.get_address_conn(addr) if config[CONF_SOURCE] in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS: - device = LcnVariableSensor(config, address_connection) + device = LcnVariableSensor(config, device_connection) else: # in LED_PORTS + LOGICOP_PORTS - device = LcnLedLogicSensor(config, address_connection) + device = LcnLedLogicSensor(config, device_connection) devices.append(device) async_add_entities(devices) -class LcnVariableSensor(LcnDevice): +class LcnVariableSensor(LcnEntity): """Representation of a LCN sensor for variables.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] self.unit = pypck.lcn_defs.VarUnit.parse(config[CONF_UNIT_OF_MEASUREMENT]) @@ -57,7 +57,7 @@ class LcnVariableSensor(LcnDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -81,12 +81,12 @@ class LcnVariableSensor(LcnDevice): self.async_write_ha_state() -class LcnLedLogicSensor(LcnDevice): +class LcnLedLogicSensor(LcnEntity): """Representation of a LCN sensor for leds and logicops.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) if config[CONF_SOURCE] in LED_PORTS: self.source = pypck.lcn_defs.LedPort[config[CONF_SOURCE]] @@ -98,7 +98,7 @@ class LcnLedLogicSensor(LcnDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 1d6f7cb6df..5891629627 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -4,7 +4,7 @@ import pypck from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS from .helpers import get_connection @@ -36,12 +36,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputSwitch(LcnDevice, SwitchEntity): +class LcnOutputSwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for output ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN switch.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] @@ -50,7 +50,7 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -59,14 +59,14 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - if not await self.address_connection.dim_output(self.output.value, 100, 0): + if not await self.device_connection.dim_output(self.output.value, 100, 0): return self._is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - if not await self.address_connection.dim_output(self.output.value, 0, 0): + if not await self.device_connection.dim_output(self.output.value, 0, 0): return self._is_on = False self.async_write_ha_state() @@ -83,12 +83,12 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): self.async_write_ha_state() -class LcnRelaySwitch(LcnDevice, SwitchEntity): +class LcnRelaySwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for relay ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN switch.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] @@ -97,7 +97,7 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -108,7 +108,7 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): """Turn the entity on.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = True self.async_write_ha_state() @@ -118,7 +118,7 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = False self.async_write_ha_state() From 180491f8cd3a1c9d8419f67278bf5573f6bd8f4f Mon Sep 17 00:00:00 2001 From: treylok Date: Sat, 5 Dec 2020 07:13:46 -0600 Subject: [PATCH 031/302] Fix Ecobee set humidity (#43954) --- homeassistant/components/ecobee/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index ccfddca4b0..94396bbf88 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate.const import ( SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) @@ -161,6 +162,7 @@ SUPPORT_FLAGS = ( | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE + | SUPPORT_TARGET_HUMIDITY ) @@ -651,7 +653,7 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity): """Set the humidity level.""" - self.data.ecobee.set_humidity(self.thermostat_index, humidity) + self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" From 9b7ecddde6fd3d212ed5f5df4214d62f713659c0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Dec 2020 15:05:12 +0100 Subject: [PATCH 032/302] Add reverse repeatmode mapping constant to Spotify (#43968) --- homeassistant/components/spotify/media_player.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ef3f1224a4..e4450e7a30 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -82,12 +82,16 @@ SUPPORT_SPOTIFY = ( | SUPPORT_VOLUME_SET ) -REPEAT_MODE_MAPPING = { +REPEAT_MODE_MAPPING_TO_HA = { "context": REPEAT_MODE_ALL, "off": REPEAT_MODE_OFF, "track": REPEAT_MODE_ONE, } +REPEAT_MODE_MAPPING_TO_SPOTIFY = { + value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items() +} + BROWSE_LIMIT = 48 MEDIA_TYPE_SHOW = "show" @@ -390,7 +394,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): def repeat(self) -> Optional[str]: """Return current repeat mode.""" repeat_state = self._currently_playing.get("repeat_state") - return REPEAT_MODE_MAPPING.get(repeat_state) + return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) @property def supported_features(self) -> int: @@ -469,9 +473,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @spotify_exception_handler def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" - for spotify, home_assistant in REPEAT_MODE_MAPPING.items(): - if home_assistant == repeat: - self._spotify.repeat(spotify) + if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: + raise ValueError(f"Unsupported repeat mode: {repeat}") + self._spotify.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) @spotify_exception_handler def update(self) -> None: From 40e5634db3366bc5e37cb9c0a4735c76cb8e7421 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 5 Dec 2020 11:20:10 -0500 Subject: [PATCH 033/302] Add ZHA Coordinator to LightLink cluster groups (#43959) * Add coordinator to LighLink cluster groups * Make pylint happy --- .../components/zha/core/channels/lightlink.py | 32 ++++++- tests/components/zha/test_channels.py | 94 ++++++++++++++++--- 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 9a357d76eb..600493e8a1 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -1,11 +1,41 @@ """Lightlink channels module for Zigbee Home Automation.""" +import asyncio + +import zigpy.exceptions import zigpy.zcl.clusters.lightlink as lightlink from .. import registries -from .base import ZigbeeChannel +from .base import ChannelStatus, ZigbeeChannel @registries.CHANNEL_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) class LightLink(ZigbeeChannel): """Lightlink channel.""" + + async def async_configure(self) -> None: + """Add Coordinator to LightLink group .""" + + if self._ch_pool.skip_configuration: + self._status = ChannelStatus.CONFIGURED + return + + application = self._ch_pool.endpoint.device.application + try: + coordinator = application.get_device(application.ieee) + except KeyError: + self.warning("Aborting - unable to locate required coordinator device.") + return + + try: + _, _, groups = await self.cluster.get_group_identifiers(0) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc: + self.warning("Couldn't get list of groups: %s", str(exc)) + return + + if groups: + for group in groups: + self.debug("Adding coordinator to 0x%04x group id", group.group_id) + await coordinator.add_to_group(group.group_id) + else: + await coordinator.add_to_group(0x0000, name="Default Lightlink Group") diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index afae1b661a..e0c31d38bb 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -14,7 +14,7 @@ import homeassistant.components.zha.core.registries as registries from .common import get_zha_gateway, make_zcl_header -import tests.async_mock +from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events @@ -38,9 +38,26 @@ async def zha_gateway(hass, setup_zha): @pytest.fixture -def channel_pool(): +def zigpy_coordinator_device(zigpy_device_mock): + """Coordinator device fixture.""" + + coordinator = zigpy_device_mock( + {1: {"in_clusters": [0x1000], "out_clusters": [], "device_type": 0x1234}}, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + with patch.object(coordinator, "add_to_group", AsyncMock(return_value=[0])): + yield coordinator + + +@pytest.fixture +def channel_pool(zigpy_coordinator_device): """Endpoint Channels fixture.""" ch_pool_mock = mock.MagicMock(spec_set=zha_channels.ChannelPool) + ch_pool_mock.endpoint.device.application.get_device.return_value = ( + zigpy_coordinator_device + ) type(ch_pool_mock).skip_configuration = mock.PropertyMock(return_value=False) ch_pool_mock.id = 1 return ch_pool_mock @@ -117,7 +134,6 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): (0x0406, 1, {"occupancy"}), (0x0702, 1, {"instantaneous_demand"}), (0x0B04, 1, {"active_power"}), - (0x1000, 1, {}), ], ) async def test_in_channel_config( @@ -174,7 +190,6 @@ async def test_in_channel_config( (0x0406, 1), (0x0702, 1), (0x0B04, 1), - (0x1000, 1), ], ) async def test_out_channel_config( @@ -386,12 +401,12 @@ async def test_ep_channels_configure(channel): ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_3 = channel(zha_const.CHANNEL_COLOR, 768) - ch_3.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) - ch_3.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) ch_4 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_5 = channel(zha_const.CHANNEL_LEVEL, 8) - ch_5.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) - ch_5.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) channels = mock.MagicMock(spec_set=zha_channels.Channels) type(channels).semaphore = mock.PropertyMock(return_value=asyncio.Semaphore(3)) @@ -427,8 +442,8 @@ async def test_poll_control_configure(poll_control_ch): async def test_poll_control_checkin_response(poll_control_ch): """Test poll control channel checkin response.""" - rsp_mock = tests.async_mock.AsyncMock() - set_interval_mock = tests.async_mock.AsyncMock() + rsp_mock = AsyncMock() + set_interval_mock = AsyncMock() cluster = poll_control_ch.cluster patch_1 = mock.patch.object(cluster, "checkin_response", rsp_mock) patch_2 = mock.patch.object(cluster, "set_long_poll_interval", set_interval_mock) @@ -449,7 +464,7 @@ async def test_poll_control_checkin_response(poll_control_ch): async def test_poll_control_cluster_command(hass, poll_control_device): """Test poll control channel response to cluster command.""" - checkin_mock = tests.async_mock.AsyncMock() + checkin_mock = AsyncMock() poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] cluster = poll_control_ch.cluster events = async_capture_events(hass, "zha_event") @@ -474,3 +489,60 @@ async def test_poll_control_cluster_command(hass, poll_control_device): assert data["args"][2] is mock.sentinel.args3 assert data["unique_id"] == "00:11:22:33:44:55:66:77:1:0x0020" assert data["device_id"] == poll_control_device.device_id + + +@pytest.fixture +def zigpy_zll_device(zigpy_device_mock): + """ZLL device fixture.""" + + return zigpy_device_mock( + {1: {"in_clusters": [0x1000], "out_clusters": [], "device_type": 0x1234}}, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + + +async def test_zll_device_groups( + zigpy_zll_device, channel_pool, zigpy_coordinator_device +): + """Test adding coordinator to ZLL groups.""" + + cluster = zigpy_zll_device.endpoints[1].lightlink + channel = zha_channels.lightlink.LightLink(cluster, channel_pool) + + with patch.object( + cluster, "command", AsyncMock(return_value=[1, 0, []]) + ) as cmd_mock: + await channel.async_configure() + assert cmd_mock.await_count == 1 + assert ( + cluster.server_commands[cmd_mock.await_args[0][0]][0] + == "get_group_identifiers" + ) + assert cluster.bind.call_count == 0 + assert zigpy_coordinator_device.add_to_group.await_count == 1 + assert zigpy_coordinator_device.add_to_group.await_args[0][0] == 0x0000 + + zigpy_coordinator_device.add_to_group.reset_mock() + group_1 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xABCD, 0x00) + group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00) + with patch.object( + cluster, "command", AsyncMock(return_value=[1, 0, [group_1, group_2]]) + ) as cmd_mock: + await channel.async_configure() + assert cmd_mock.await_count == 1 + assert ( + cluster.server_commands[cmd_mock.await_args[0][0]][0] + == "get_group_identifiers" + ) + assert cluster.bind.call_count == 0 + assert zigpy_coordinator_device.add_to_group.await_count == 2 + assert ( + zigpy_coordinator_device.add_to_group.await_args_list[0][0][0] + == group_1.group_id + ) + assert ( + zigpy_coordinator_device.add_to_group.await_args_list[1][0][0] + == group_2.group_id + ) From a1720fdd2b99e01ce078b851c12bbc4f5b424124 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 5 Dec 2020 18:24:49 -0500 Subject: [PATCH 034/302] Cleanup ZHA fan channel (#43973) * Use zigpy cached values for ZHA Fan speed * Disable update_before_add for ZHA fans * Refresh state of the group * Fix group tests --- .../components/zha/core/channels/hvac.py | 19 +-- homeassistant/components/zha/fan.py | 39 ++--- tests/components/zha/test_fan.py | 145 ++++++++++++++---- 3 files changed, 135 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index f8f04414fa..1647c5ce52 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -43,17 +43,10 @@ class FanChannel(ZigbeeChannel): REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType - ): - """Init Thermostat channel instance.""" - super().__init__(cluster, ch_pool) - self._fan_mode = None - @property def fan_mode(self) -> Optional[int]: """Return current fan mode.""" - return self._fan_mode + return self.cluster.get("fan_mode") async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" @@ -66,12 +59,7 @@ class FanChannel(ZigbeeChannel): async def async_update(self) -> None: """Retrieve latest state.""" - result = await self.get_attribute_value("fan_mode", from_cache=True) - if result is not None: - self._fan_mode = result - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "fan_mode", result - ) + await self.get_attribute_value("fan_mode", from_cache=False) @callback def attribute_updated(self, attrid: int, value: Any) -> None: @@ -80,8 +68,7 @@ class FanChannel(ZigbeeChannel): self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) - if attrid == self._value_attribute: - self._fan_mode = value + if attr_name == "fan_mode": self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 9983967f76..b25d1c1aa3 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,6 +1,6 @@ """Fans on Zigbee Home Automation networks.""" import functools -from typing import List +from typing import List, Optional from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.hvac as hvac @@ -62,7 +62,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, ), ) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) @@ -87,13 +90,6 @@ class BaseFan(FanEntity): """Return the current speed.""" return self._state - @property - def is_on(self) -> bool: - """Return true if entity is on.""" - if self._state is None: - return False - return self._state != SPEED_OFF - @property def supported_features(self) -> int: """Flag supported features.""" @@ -136,25 +132,16 @@ class ZhaFan(BaseFan, ZhaEntity): self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._state = VALUE_TO_SPEED.get(last_state.state, last_state.state) + @property + def speed(self) -> Optional[str]: + """Return the current speed.""" + return VALUE_TO_SPEED.get(self._fan_channel.fan_mode) @callback def async_set_state(self, attr_id, attr_name, value): """Handle state update from channel.""" - self._state = VALUE_TO_SPEED.get(value, self._state) self.async_write_ha_state() - async def async_update(self): - """Attempt to retrieve on off state from the fan.""" - await super().async_update() - if self._fan_channel: - state = await self._fan_channel.get_attribute_value("fan_mode") - if state is not None: - self._state = VALUE_TO_SPEED.get(state, self._state) - @GROUP_MATCH() class FanGroup(BaseFan, ZhaGroupEntity): @@ -185,9 +172,15 @@ class FanGroup(BaseFan, ZhaGroupEntity): all_states = [self.hass.states.get(x) for x in self._entity_ids] states: List[State] = list(filter(None, all_states)) on_states: List[State] = [state for state in states if state.state != SPEED_OFF] + self._available = any(state.state != STATE_UNAVAILABLE for state in states) # for now just use first non off state since its kind of arbitrary if not on_states: self._state = SPEED_OFF else: - self._state = states[0].state + self._state = on_states[0].state + + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + await self.async_update() + await super().async_added_to_hass() diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index b12b924937..65be13fd96 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -3,6 +3,7 @@ import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.hvac as hvac +import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ( @@ -10,11 +11,13 @@ from homeassistant.components.fan import ( DOMAIN, SERVICE_SET_SPEED, SPEED_HIGH, + SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.zha.core.discovery import GROUP_PROBE +from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -23,6 +26,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.setup import async_setup_component from .common import ( async_enable_traffic, @@ -33,7 +37,7 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import call +from tests.async_mock import AsyncMock, call, patch IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @@ -49,7 +53,9 @@ def zigpy_device(zigpy_device_mock): "device_type": zha.DeviceType.ON_OFF_SWITCH, } } - return zigpy_device_mock(endpoints) + return zigpy_device_mock( + endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" + ) @pytest.fixture @@ -59,7 +65,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [], + "in_clusters": [general.Groups.cluster_id], "out_clusters": [], "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, } @@ -80,14 +86,20 @@ async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [general.OnOff.cluster_id, hvac.Fan.cluster_id], + "in_clusters": [ + general.Groups.cluster_id, + general.OnOff.cluster_id, + hvac.Fan.cluster_id, + ], "out_clusters": [], - } + "device_type": zha.DeviceType.ON_OFF_LIGHT, + }, }, ieee=IEEE_GROUPABLE_DEVICE, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -99,17 +111,20 @@ async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): { 1: { "in_clusters": [ + general.Groups.cluster_id, general.OnOff.cluster_id, hvac.Fan.cluster_id, general.LevelControl.cluster_id, ], "out_clusters": [], - } + "device_type": zha.DeviceType.ON_OFF_LIGHT, + }, }, ieee=IEEE_GROUPABLE_DEVICE2, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -191,9 +206,11 @@ async def async_set_speed(hass, entity_id, speed=None): await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) -async def async_test_zha_group_fan_entity( - hass, device_fan_1, device_fan_2, coordinator -): +@patch( + "zigpy.zcl.clusters.hvac.Fan.write_attributes", + new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]), +) +async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinator): """Test the fan entity for a ZHA group.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None @@ -202,19 +219,20 @@ async def async_test_zha_group_fan_entity( device_fan_1._zha_gateway = zha_gateway device_fan_2._zha_gateway = zha_gateway member_ieee_addresses = [device_fan_1.ieee, device_fan_2.ieee] + members = [GroupMember(device_fan_1.ieee, 1), GroupMember(device_fan_2.ieee, 1)] # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 2 for member in zha_group.members: - assert member.ieee in member_ieee_addresses + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None - entity_domains = GROUP_PROBE.determine_entity_domains(zha_group) + entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) assert len(entity_domains) == 2 assert LIGHT_DOMAIN in entity_domains @@ -224,14 +242,17 @@ async def async_test_zha_group_fan_entity( assert hass.states.get(entity_id) is not None group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] - dev1_fan_cluster = device_fan_1.endpoints[1].fan - dev2_fan_cluster = device_fan_2.endpoints[1].fan - # test that the lights were created and that they are unavailable + dev1_fan_cluster = device_fan_1.device.endpoints[1].fan + dev2_fan_cluster = device_fan_2.device.endpoints[1].fan + + await async_enable_traffic(hass, [device_fan_1, device_fan_2], enabled=False) + await hass.async_block_till_done() + # test that the fans were created and that they are 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_group.members) + await async_enable_traffic(hass, [device_fan_1, device_fan_2]) # test that the fan group entity was created and is off assert hass.states.get(entity_id).state == STATE_OFF @@ -239,37 +260,103 @@ async def async_test_zha_group_fan_entity( # turn on from HA group_fan_cluster.write_attributes.reset_mock() await async_turn_on(hass, entity_id) + await hass.async_block_till_done() assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 2}) - assert hass.states.get(entity_id).state == SPEED_MEDIUM + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 2} # turn off from HA group_fan_cluster.write_attributes.reset_mock() await async_turn_off(hass, entity_id) assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 0}) - assert hass.states.get(entity_id).state == STATE_OFF + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 0} # change speed from HA group_fan_cluster.write_attributes.reset_mock() await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 3}) - assert hass.states.get(entity_id).state == SPEED_HIGH + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 3} # test some of the group logic to make sure we key off states correctly - await dev1_fan_cluster.async_set_speed(SPEED_OFF) - await dev2_fan_cluster.async_set_speed(SPEED_OFF) + await send_attributes_report(hass, dev1_fan_cluster, {0: 0}) + await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) # test that group fan is off assert hass.states.get(entity_id).state == STATE_OFF - await dev1_fan_cluster.async_set_speed(SPEED_MEDIUM) + await send_attributes_report(hass, dev2_fan_cluster, {0: 2}) + await hass.async_block_till_done() # test that group fan is speed medium - assert hass.states.get(entity_id).state == SPEED_MEDIUM + assert hass.states.get(entity_id).state == STATE_ON - await dev1_fan_cluster.async_set_speed(SPEED_OFF) + await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) + await hass.async_block_till_done() # test that group fan is now off assert hass.states.get(entity_id).state == STATE_OFF + + +@pytest.mark.parametrize( + "plug_read, expected_state, expected_speed", + ( + (None, STATE_OFF, None), + ({"fan_mode": 0}, STATE_OFF, SPEED_OFF), + ({"fan_mode": 1}, STATE_ON, SPEED_LOW), + ({"fan_mode": 2}, STATE_ON, SPEED_MEDIUM), + ({"fan_mode": 3}, STATE_ON, SPEED_HIGH), + ), +) +async def test_fan_init( + hass, + zha_device_joined_restored, + zigpy_device, + plug_read, + expected_state, + expected_speed, +): + """Test zha fan platform.""" + + cluster = zigpy_device.endpoints.get(1).fan + cluster.PLUGGED_ATTR_READS = plug_read + + 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 + assert hass.states.get(entity_id).state == expected_state + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == expected_speed + + +async def test_fan_update_entity( + hass, + zha_device_joined_restored, + zigpy_device, +): + """Test zha fan platform.""" + + cluster = zigpy_device.endpoints.get(1).fan + cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} + + 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 + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF + assert cluster.read_attributes.await_count == 1 + + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF + assert cluster.read_attributes.await_count == 2 + + cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW + assert cluster.read_attributes.await_count == 3 From f9bc7bacb6a6fa3e1bda59c5e68dbbd76d92b392 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 6 Dec 2020 00:03:59 +0000 Subject: [PATCH 035/302] [ci skip] Translation update --- homeassistant/components/apple_tv/translations/cs.json | 3 +++ homeassistant/components/apple_tv/translations/no.json | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json index 59f83e0272..98169067fb 100644 --- a/homeassistant/components/apple_tv/translations/cs.json +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -14,6 +14,9 @@ }, "flow_title": "Apple TV: {name}", "step": { + "confirm": { + "title": "Potvrzen\u00ed p\u0159id\u00e1n\u00ed Apple TV" + }, "pair_no_pin": { "title": "P\u00e1rov\u00e1n\u00ed" }, diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index 974fe2ce5f..88a7c98615 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -4,7 +4,7 @@ "already_configured_device": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "backoff": "Enheten godtar ikke parringsanmodninger for \u00f8yeblikket (du har kanskje angitt en ugyldig PIN-kode for mange ganger), pr\u00f8v igjen senere.", - "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten.", + "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten", "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "unknown": "Uventet feil" @@ -34,11 +34,11 @@ "title": "Sammenkobling" }, "reconfigure": { - "description": "Denne Apple TV har noen tilkoblingsvansker og m\u00e5 konfigureres p\u00e5 nytt.", + "description": "Denne Apple TVen har noen tilkoblingsvansker og m\u00e5 konfigureres p\u00e5 nytt", "title": "Omkonfigurering av enheter" }, "service_problem": { - "description": "Det oppstod et problem under sammenkobling av protokollen \" {protocol} \". Det vil bli ignorert.", + "description": "Det oppstod et problem under sammenkobling av protokollen \"{protocol}\". Det vil bli ignorert.", "title": "Kunne ikke legge til tjenesten" }, "user": { From 23f8ae8fecc6d82f8208dbd42d14c4a3633fd630 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Dec 2020 17:24:32 +0100 Subject: [PATCH 036/302] Update ring to 0.6.2 (#43995) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index d46f12af51..550da4d38e 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.6.0"], + "requirements": ["ring_doorbell==0.6.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index da06aa3cd2..9e599f62c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ rfk101py==0.0.1 rflink==0.0.55 # homeassistant.components.ring -ring_doorbell==0.6.0 +ring_doorbell==0.6.2 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36c65ae0ff..9f9775cbd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ restrictedpython==5.0 rflink==0.0.55 # homeassistant.components.ring -ring_doorbell==0.6.0 +ring_doorbell==0.6.2 # homeassistant.components.roku rokuecp==0.6.0 From 8b01f681ab3106708aed9f4b5fe64c23c6b4d184 Mon Sep 17 00:00:00 2001 From: Jacob Southard Date: Sun, 6 Dec 2020 17:23:08 -0600 Subject: [PATCH 037/302] Add target temperature range to homekit_controller (#42817) * Add support for temperature range in thermostat. * Add tests for setting temperature range. * Update Lennox E30/Ecobee 3 tests to reflect new supported feature * Add support for thermostate mode specific min/max temp values. --- .../components/homekit_controller/climate.py | 78 ++++++++-- .../specific_devices/test_ecobee3.py | 5 +- .../specific_devices/test_lennox_e30.py | 7 +- .../homekit_controller/test_climate.py | 136 ++++++++++++++++++ 4 files changed, 213 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index ed2d3c74d7..042bc4771c 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -19,6 +19,8 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -30,6 +32,7 @@ from homeassistant.components.climate.const import ( SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, SWING_OFF, SWING_VERTICAL, ) @@ -329,7 +332,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): return [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_CURRENT, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_TARGET, CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET, @@ -338,10 +343,23 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - - await self.async_put_characteristics( - {CharacteristicsTypes.TEMPERATURE_TARGET: temp} - ) + heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + if temp is None: + temp = (cool_temp + heat_temp) / 2 + await self.async_put_characteristics( + { + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: heat_temp, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: cool_temp, + CharacteristicsTypes.TEMPERATURE_TARGET: temp, + } + ) + else: + await self.async_put_characteristics( + {CharacteristicsTypes.TEMPERATURE_TARGET: temp} + ) async def async_set_humidity(self, humidity): """Set new target humidity.""" @@ -367,22 +385,57 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): @property def target_temperature(self): """Return the temperature we try to reach.""" + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + return None return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET) + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}: + return None + return self.service.value(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}: + return None + return self.service.value(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD) + @property def min_temp(self): """Return the minimum target temp.""" - if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): - char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET] - return char.minValue + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + min_temp = self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].minValue + if min_temp is not None: + return min_temp + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + min_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].minValue + if min_temp is not None: + return min_temp return super().min_temp @property def max_temp(self): """Return the maximum target temp.""" - if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): - char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET] - return char.maxValue + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + max_temp = self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].maxValue + if max_temp is not None: + return max_temp + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + max_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].maxValue + if max_temp is not None: + return max_temp return super().max_temp @property @@ -443,6 +496,11 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): features |= SUPPORT_TARGET_TEMPERATURE + if self.service.has( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ) and self.service.has(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD): + features |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self.service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET): features |= SUPPORT_TARGET_HUMIDITY diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index c1a956f3f4..d05e36ed0e 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -12,6 +12,7 @@ from aiohomekit.testing import FakePairing from homeassistant.components.climate.const import ( SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY @@ -40,7 +41,9 @@ async def test_ecobee3_setup(hass): climate_state = await climate_helper.poll_and_get_state() assert climate_state.attributes["friendly_name"] == "HomeW" assert climate_state.attributes["supported_features"] == ( - SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_TARGET_HUMIDITY ) assert climate_state.attributes["hvac_modes"] == [ diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index fe7b0c7783..a49effdb75 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -4,7 +4,10 @@ Regression tests for Aqara Gateway V3. https://github.com/home-assistant/core/issues/20885 """ -from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) from tests.components.homekit_controller.common import ( Helper, @@ -29,7 +32,7 @@ async def test_lennox_e30_setup(hass): climate_state = await climate_helper.poll_and_get_state() assert climate_state.attributes["friendly_name"] == "Lennox" assert climate_state.attributes["supported_features"] == ( - SUPPORT_TARGET_TEMPERATURE + SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE ) device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 38156354cd..d3f852d7a4 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -24,6 +24,14 @@ from tests.components.homekit_controller.common import setup_test_component HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target") HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current") +THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD = ( + "thermostat", + "temperature.cooling-threshold", +) +THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD = ( + "thermostat", + "temperature.heating-threshold", +) TEMPERATURE_TARGET = ("thermostat", "temperature.target") TEMPERATURE_CURRENT = ("thermostat", "temperature.current") HUMIDITY_TARGET = ("thermostat", "relative-humidity.target") @@ -42,6 +50,16 @@ def create_thermostat_service(accessory): char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT) char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) + char.minValue = 15 + char.maxValue = 40 + char.value = 0 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD) + char.minValue = 4 + char.maxValue = 30 + char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_TARGET) char.minValue = 7 char.maxValue = 35 @@ -126,6 +144,41 @@ async def test_climate_change_thermostat_state(hass, utcnow): assert helper.characteristics[HEATING_COOLING_TARGET].value == 0 +async def test_climate_check_min_max_values_per_mode(hass, utcnow): + """Test that we we get the appropriate min/max values for each mode.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 4 + assert climate_state.attributes["max_temp"] == 40 + + async def test_climate_change_thermostat_temperature(hass, utcnow): """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -147,6 +200,89 @@ async def test_climate_change_thermostat_temperature(hass, utcnow): assert helper.characteristics[TEMPERATURE_TARGET].value == 25 +async def test_climate_change_thermostat_temperature_range(hass, utcnow): + """Test that we can set separate heat and cool setpoints in heat_cool mode.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "target_temp_high": 25, + "target_temp_low": 20, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22.5 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 25 + + +async def test_climate_change_thermostat_temperature_range_iphone(hass, utcnow): + """Test that we can set all three set points at once (iPhone heat_cool mode support).""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "temperature": 22, + "target_temp_low": 20, + "target_temp_high": 24, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 24 + + +async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcnow): + """Test that we cannot set range values when not in heat_cool mode.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "temperature": 22, + "target_temp_low": 20, + "target_temp_high": 24, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 0 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 0 + + async def test_climate_change_thermostat_humidity(hass, utcnow): """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) From 9c63fbfcb1b70c9f1720aa45784a1164ba029921 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 7 Dec 2020 00:04:18 +0000 Subject: [PATCH 038/302] [ci skip] Translation update --- .../components/abode/translations/pt.json | 13 ++++ .../accuweather/translations/cs.json | 6 ++ .../accuweather/translations/pt.json | 5 +- .../components/acmeda/translations/pt.json | 7 +++ .../components/adguard/translations/pt.json | 7 ++- .../advantage_air/translations/pt.json | 19 ++++++ .../components/agent_dvr/translations/pt.json | 9 ++- .../components/airly/translations/cs.json | 5 ++ .../components/airly/translations/pt.json | 6 +- .../components/airvisual/translations/pt.json | 22 +++++++ .../alarmdecoder/translations/pt.json | 33 ++++++++++ .../components/almond/translations/pt.json | 4 ++ .../ambiclimate/translations/pt.json | 8 +++ .../components/apple_tv/translations/cs.json | 17 +++++- .../components/apple_tv/translations/pt.json | 61 +++++++++++++++++++ .../components/arcam_fmj/translations/pt.json | 3 + .../components/atag/translations/pt.json | 6 ++ .../components/august/translations/pt.json | 3 + .../components/aurora/translations/pt.json | 3 + .../components/awair/translations/pt.json | 5 ++ .../components/axis/translations/pt.json | 4 +- .../azure_devops/translations/pt.json | 4 ++ .../binary_sensor/translations/pt.json | 16 +++++ .../components/blebox/translations/pt.json | 1 + .../components/blink/translations/pt.json | 3 +- .../components/braviatv/translations/pt.json | 3 + .../components/broadlink/translations/pt.json | 3 + .../components/brother/translations/pt.json | 1 + .../components/bsblan/translations/pt.json | 7 ++- .../components/canary/translations/pt.json | 19 ++++++ .../cert_expiry/translations/pt.json | 4 +- .../cloudflare/translations/pt.json | 19 ++++++ .../coolmaster/translations/pt.json | 3 + .../components/daikin/translations/pt.json | 6 ++ .../components/deconz/translations/pt.json | 8 ++- .../components/denonavr/translations/pt.json | 10 +++ .../devolo_home_control/translations/pt.json | 11 +++- .../components/dexcom/translations/pt.json | 5 ++ .../dialogflow/translations/pt.json | 4 ++ .../components/doorbird/translations/pt.json | 7 ++- .../components/dunehd/translations/pt.json | 7 +++ .../components/ecobee/translations/pt.json | 6 +- .../components/elgato/translations/pt.json | 6 ++ .../emulated_roku/translations/pt.json | 3 + .../components/epson/translations/pt.json | 16 +++++ .../components/esphome/translations/pt.json | 5 +- .../fireservicerota/translations/pt.json | 27 ++++++++ .../flick_electric/translations/pt.json | 3 +- .../flunearyou/translations/pt.json | 3 + .../forked_daapd/translations/pt.json | 7 ++- .../components/freebox/translations/pt.json | 3 + .../components/fritzbox/translations/pt.json | 8 +++ .../components/geofency/translations/pt.json | 4 ++ .../geonetnz_volcano/translations/pt.json | 3 + .../components/gios/translations/pt.json | 11 ++++ .../components/glances/translations/pt.json | 5 +- .../components/goalzero/translations/pt.json | 12 ++++ .../components/gpslogger/translations/pt.json | 4 ++ .../components/gree/translations/pt.json | 13 ++++ .../components/guardian/translations/pt.json | 4 ++ .../components/harmony/translations/pt.json | 4 ++ .../components/hassio/translations/pt.json | 5 +- .../components/heos/translations/pt.json | 6 ++ .../home_connect/translations/pt.json | 12 ++++ .../homeassistant/translations/pt.json | 5 +- .../components/homekit/translations/pt.json | 43 +++++++++++++ .../homekit_controller/translations/pt.json | 20 ++++++ .../huawei_lte/translations/pt.json | 4 +- .../components/hue/translations/pt.json | 1 + .../translations/pt.json | 4 ++ .../components/hyperion/translations/pt.json | 21 +++++++ .../components/iaqualink/translations/pt.json | 6 ++ .../components/icloud/translations/pt.json | 13 ++++ .../components/ifttt/translations/pt.json | 4 ++ .../components/ipma/translations/pt.json | 5 ++ .../components/ipp/translations/pt.json | 8 ++- .../components/iqvia/translations/pt.json | 7 +++ .../islamic_prayer_times/translations/pt.json | 7 +++ .../components/isy994/translations/pt.json | 4 +- .../components/juicenet/translations/pt.json | 12 ++++ .../components/kodi/translations/cs.json | 3 +- .../components/konnected/translations/pt.json | 39 ++++++++++++ .../components/kulersky/translations/pt.json | 13 ++++ .../components/life360/translations/pt.json | 9 ++- .../components/local_ip/translations/pt.json | 9 +++ .../components/locative/translations/pt.json | 4 ++ .../logi_circle/translations/pt.json | 12 ++++ .../components/luftdaten/translations/pt.json | 2 + .../components/mailgun/translations/pt.json | 4 ++ .../components/met/translations/pt.json | 3 + .../components/metoffice/translations/pt.json | 13 ++++ .../components/mikrotik/translations/pt.json | 4 ++ .../components/mill/translations/pt.json | 6 +- .../mobile_app/translations/pt.json | 5 ++ .../motion_blinds/translations/pt.json | 17 ++++++ .../components/mqtt/translations/cs.json | 6 +- .../components/myq/translations/pt.json | 4 ++ .../components/neato/translations/pt.json | 7 +++ .../components/nest/translations/pt.json | 13 +++- .../components/netatmo/translations/pt.json | 6 ++ .../components/nexia/translations/pt.json | 5 ++ .../nightscout/translations/pt.json | 8 +++ .../components/notion/translations/pt.json | 3 + .../components/omnilogic/translations/pt.json | 20 ++++++ .../components/onewire/translations/pt.json | 18 ++++++ .../components/onvif/translations/pt.json | 3 + .../opentherm_gw/translations/pt.json | 3 + .../openweathermap/translations/pt.json | 8 +++ .../ovo_energy/translations/pt.json | 9 ++- .../components/owntracks/translations/pt.json | 3 + .../components/ozw/translations/pt.json | 16 +++++ .../panasonic_viera/translations/pt.json | 10 +++ .../components/pi_hole/translations/pt.json | 7 ++- .../components/plaato/translations/pt.json | 8 +++ .../components/plex/translations/pt.json | 5 +- .../components/plugwise/translations/pt.json | 10 +++ .../components/point/translations/pt.json | 3 +- .../components/powerwall/translations/pt.json | 11 ++++ .../components/profiler/translations/pt.json | 12 ++++ .../progettihwsw/translations/pt.json | 3 + .../components/ps4/translations/pt.json | 2 + .../components/rachio/translations/pt.json | 6 +- .../rainmachine/translations/pt.json | 3 + .../recollect_waste/translations/pt.json | 3 + .../components/rfxtrx/translations/pt.json | 22 +++++++ .../components/rpi_power/translations/pt.json | 12 ++++ .../ruckus_unleashed/translations/pt.json | 21 +++++++ .../components/samsungtv/translations/pt.json | 3 + .../components/sentry/translations/cs.json | 3 +- .../components/sharkiq/translations/pt.json | 12 ++++ .../components/shelly/translations/pt.json | 7 +++ .../simplisafe/translations/pt.json | 7 ++- .../components/smappee/translations/pt.json | 7 ++- .../components/smarthab/translations/pt.json | 4 ++ .../smartthings/translations/pt.json | 3 + .../components/solaredge/translations/pt.json | 9 ++- .../components/somfy/translations/pt.json | 7 +++ .../components/sonarr/translations/pt.json | 18 +++++- .../components/songpal/translations/pt.json | 7 +++ .../speedtestdotnet/translations/pt.json | 12 ++++ .../components/spotify/translations/pt.json | 1 + .../srp_energy/translations/pt.json | 20 ++++++ .../synology_dsm/translations/pt.json | 13 +++- .../components/tasmota/translations/pt.json | 18 ++++++ .../tellduslive/translations/pt.json | 6 +- .../components/tesla/translations/pt.json | 5 ++ .../components/tibber/translations/pt.json | 6 ++ .../components/tile/translations/pt.json | 3 + .../components/toon/translations/pt.json | 4 +- .../totalconnect/translations/pt.json | 3 + .../components/traccar/translations/pt.json | 8 +++ .../transmission/translations/pt.json | 4 ++ .../components/tuya/translations/pt.json | 10 ++- .../twentemilieu/translations/pt.json | 10 +++ .../components/twilio/translations/pt.json | 4 ++ .../components/twinkly/translations/pt.json | 10 +++ .../components/upb/translations/pt.json | 3 + .../components/upcloud/translations/pt.json | 16 +++++ .../components/vesync/translations/pt.json | 6 ++ .../components/vilfo/translations/pt.json | 1 + .../components/vizio/translations/pt.json | 3 + .../water_heater/translations/pt.json | 8 +++ .../components/wiffi/translations/pt.json | 11 ++++ .../components/withings/translations/pt.json | 6 ++ .../components/wled/translations/pt.json | 6 ++ .../components/wolflink/translations/pt.json | 5 ++ .../components/xbox/translations/pt.json | 17 ++++++ .../xiaomi_miio/translations/pt.json | 6 +- .../components/yeelight/translations/cs.json | 4 +- .../components/yeelight/translations/pt.json | 3 + .../components/zerproc/translations/pt.json | 9 +++ .../zoneminder/translations/pt.json | 22 +++++++ .../components/zwave/translations/pt.json | 3 +- 173 files changed, 1437 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/acmeda/translations/pt.json create mode 100644 homeassistant/components/advantage_air/translations/pt.json create mode 100644 homeassistant/components/alarmdecoder/translations/pt.json create mode 100644 homeassistant/components/ambiclimate/translations/pt.json create mode 100644 homeassistant/components/apple_tv/translations/pt.json create mode 100644 homeassistant/components/canary/translations/pt.json create mode 100644 homeassistant/components/cloudflare/translations/pt.json create mode 100644 homeassistant/components/epson/translations/pt.json create mode 100644 homeassistant/components/fireservicerota/translations/pt.json create mode 100644 homeassistant/components/gios/translations/pt.json create mode 100644 homeassistant/components/goalzero/translations/pt.json create mode 100644 homeassistant/components/gree/translations/pt.json create mode 100644 homeassistant/components/home_connect/translations/pt.json create mode 100644 homeassistant/components/homekit/translations/pt.json create mode 100644 homeassistant/components/hyperion/translations/pt.json create mode 100644 homeassistant/components/iqvia/translations/pt.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/pt.json create mode 100644 homeassistant/components/kulersky/translations/pt.json create mode 100644 homeassistant/components/local_ip/translations/pt.json create mode 100644 homeassistant/components/logi_circle/translations/pt.json create mode 100644 homeassistant/components/metoffice/translations/pt.json create mode 100644 homeassistant/components/motion_blinds/translations/pt.json create mode 100644 homeassistant/components/omnilogic/translations/pt.json create mode 100644 homeassistant/components/onewire/translations/pt.json create mode 100644 homeassistant/components/ozw/translations/pt.json create mode 100644 homeassistant/components/plaato/translations/pt.json create mode 100644 homeassistant/components/profiler/translations/pt.json create mode 100644 homeassistant/components/rpi_power/translations/pt.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/pt.json create mode 100644 homeassistant/components/somfy/translations/pt.json create mode 100644 homeassistant/components/songpal/translations/pt.json create mode 100644 homeassistant/components/speedtestdotnet/translations/pt.json create mode 100644 homeassistant/components/srp_energy/translations/pt.json create mode 100644 homeassistant/components/tasmota/translations/pt.json create mode 100644 homeassistant/components/traccar/translations/pt.json create mode 100644 homeassistant/components/twentemilieu/translations/pt.json create mode 100644 homeassistant/components/twinkly/translations/pt.json create mode 100644 homeassistant/components/upcloud/translations/pt.json create mode 100644 homeassistant/components/water_heater/translations/pt.json create mode 100644 homeassistant/components/wiffi/translations/pt.json create mode 100644 homeassistant/components/xbox/translations/pt.json create mode 100644 homeassistant/components/zerproc/translations/pt.json create mode 100644 homeassistant/components/zoneminder/translations/pt.json diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 0df67a9418..cc0e1ef971 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -1,6 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe", + "username": "Email" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json index d7d7d6e9b3..ea954b9f0d 100644 --- a/homeassistant/components/accuweather/translations/cs.json +++ b/homeassistant/components/accuweather/translations/cs.json @@ -31,5 +31,11 @@ "title": "Mo\u017enosti AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Lze kontaktovat AccuWeather server", + "remaining_requests": "Zb\u00fdvaj\u00edc\u00ed povolen\u00e9 \u017e\u00e1dosti" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 084965331e..14260bd572 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -10,9 +10,10 @@ "step": { "user": { "data": { - "api_key": "Chave de API", + "api_key": "API Key", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "name": "Nome" } } } diff --git a/homeassistant/components/acmeda/translations/pt.json b/homeassistant/components/acmeda/translations/pt.json new file mode 100644 index 0000000000..8fcd9c1342 --- /dev/null +++ b/homeassistant/components/acmeda/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index 6f56d996b6..2ae53598eb 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "hassio_confirm": { "title": "AdGuard Home via Hass.io add-on" @@ -9,7 +12,9 @@ "host": "Servidor", "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar certificado SSL" } } } diff --git a/homeassistant/components/advantage_air/translations/pt.json b/homeassistant/components/advantage_air/translations/pt.json new file mode 100644 index 0000000000..37e27fd839 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "port": "Porta" + }, + "title": "Ligar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/pt.json b/homeassistant/components/agent_dvr/translations/pt.json index ce7cbc3f54..fa5aa3de31 100644 --- a/homeassistant/components/agent_dvr/translations/pt.json +++ b/homeassistant/components/agent_dvr/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/airly/translations/cs.json b/homeassistant/components/airly/translations/cs.json index 86d678d31a..8b35399bcb 100644 --- a/homeassistant/components/airly/translations/cs.json +++ b/homeassistant/components/airly/translations/cs.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Lze kontaktovat Airly server" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index ae35beabf6..7a80a1b350 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -1,11 +1,15 @@ { "config": { + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { "api_key": "", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "name": "Nome" }, "title": "" } diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index f7830dbe18..d6732cdddc 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -1,10 +1,32 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada ou Node/Pro ID j\u00e1 est\u00e1 registrado.", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "general_error": "Erro inesperado", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { + "geography": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + } + }, "node_pro": { "data": { + "ip_address": "Servidor", "password": "Palavra-passe" } + }, + "reauth_confirm": { + "data": { + "api_key": "" + } } } } diff --git a/homeassistant/components/alarmdecoder/translations/pt.json b/homeassistant/components/alarmdecoder/translations/pt.json new file mode 100644 index 0000000000..8d6cb9a2eb --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/pt.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "protocol": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "zone_details": { + "data": { + "zone_name": "Nome da Zona", + "zone_type": "Tipo de Zona" + } + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero da Zona" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pt.json b/homeassistant/components/almond/translations/pt.json index 94dfbefb86..fa5dc98c8f 100644 --- a/homeassistant/components/almond/translations/pt.json +++ b/homeassistant/components/almond/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" diff --git a/homeassistant/components/ambiclimate/translations/pt.json b/homeassistant/components/ambiclimate/translations/pt.json new file mode 100644 index 0000000000..bb9215f039 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json index 98169067fb..0314afa1f5 100644 --- a/homeassistant/components/apple_tv/translations/cs.json +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -18,27 +18,42 @@ "title": "Potvrzen\u00ed p\u0159id\u00e1n\u00ed Apple TV" }, "pair_no_pin": { + "description": "Pro slu\u017ebu `{protocol}` je vy\u017eadov\u00e1no p\u00e1rov\u00e1n\u00ed. Pokra\u010dujte zad\u00e1n\u00edm k\u00f3du PIN {pin} na Apple TV.", "title": "P\u00e1rov\u00e1n\u00ed" }, "pair_with_pin": { "data": { "pin": "PIN k\u00f3d" }, + "description": "U protokolu `{protocol}` je vy\u017eadov\u00e1no p\u00e1rov\u00e1n\u00ed. Zadejte pros\u00edm PIN k\u00f3d zobrazen\u00fd na obrazovce. \u00davodn\u00ed nuly mus\u00ed b\u00fdt vynech\u00e1ny, tj. zadejte 123, pokud je zobrazen\u00fd k\u00f3d 0123.", "title": "P\u00e1rov\u00e1n\u00ed" }, "reconfigure": { - "description": "U t\u00e9to Apple TV doch\u00e1z\u00ed k probl\u00e9m\u016fm s p\u0159ipojen\u00edm a je t\u0159eba ji znovu nastavit." + "description": "U t\u00e9to Apple TV doch\u00e1z\u00ed k probl\u00e9m\u016fm s p\u0159ipojen\u00edm a je t\u0159eba ji znovu nastavit.", + "title": "Zm\u011bna konfigurace za\u0159\u00edzen\u00ed" }, "service_problem": { + "description": "P\u0159i p\u00e1rov\u00e1n\u00ed protokolu `{protocol}` do\u0161lo k probl\u00e9mu. Protokol bude ignorov\u00e1n.", "title": "Nepoda\u0159ilo se p\u0159idat slu\u017ebu" }, "user": { "data": { "device_input": "Za\u0159\u00edzen\u00ed" }, + "description": "Za\u010dn\u011bte zad\u00e1n\u00edm n\u00e1zvu za\u0159\u00edzen\u00ed (nap\u0159. Kuchyn\u011b nebo lo\u017enice) nebo IP adresy Apple TV, kterou chcete p\u0159idat. Pokud byla ve va\u0161\u00ed s\u00edti automaticky nalezena n\u011bkter\u00e1 za\u0159\u00edzen\u00ed, jsou uvedena n\u00ed\u017ee. \n\n Pokud nevid\u00edte sv\u00e9 za\u0159\u00edzen\u00ed nebo nastaly n\u011bjak\u00e9 probl\u00e9my, zkuste zadat IP adresu za\u0159\u00edzen\u00ed. \n\n {devices}", "title": "Nastaven\u00ed nov\u00e9 Apple TV" } } }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nezap\u00ednejte za\u0159\u00edzen\u00ed dokud se Home Assistant spou\u0161t\u00ed" + }, + "description": "Konfigurace obecn\u00fdch mo\u017enost\u00ed za\u0159\u00edzen\u00ed" + } + } + }, "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json new file mode 100644 index 0000000000..486ff0c51e --- /dev/null +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -0,0 +1,61 @@ +{ + "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "unknown": "Erro inesperado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_usable_service": "Foi encontrado um dispositivo, mas n\u00e3o foi poss\u00edvel identificar nenhuma forma de estabelecer uma liga\u00e7\u00e3o com ele. Se continuar a ver esta mensagem, tente especificar o endere\u00e7o IP ou reiniciar a sua Apple TV.", + "unknown": "Erro inesperado" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e1 prestes a adicionar a Apple TV com o nome `{name}` ao Home Assistant.\n\n** Para completar o processo, poder\u00e1 ter que inserir v\u00e1rios c\u00f3digos PIN.**\n\nNote que *n\u00e3o* conseguir\u00e1 desligar a sua Apple TV com esta integra\u00e7\u00e3o. Apenas o media player no Home Assistant ser\u00e1 desligado!", + "title": "Confirme a adi\u00e7\u00e3o da Apple TV" + }, + "pair_no_pin": { + "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN {pin} na sua Apple TV para continuar.", + "title": "Emparelhamento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN exibido no ecran. Os zeros iniciais devem ser omitidos, ou seja, digite 123 se o c\u00f3digo exibido for 0123.", + "title": "Emparelhamento" + }, + "reconfigure": { + "description": "Esta Apple TV apresenta dificuldades de liga\u00e7\u00e3o e precisa ser reconfigurada.", + "title": "Reconfigura\u00e7\u00e3o do dispositivo" + }, + "service_problem": { + "description": "Ocorreu um problema durante o protocolo de emparelhamento `{protocol}`. Ser\u00e1 ignorado.", + "title": "Falha ao adicionar servi\u00e7o" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.\n\n{devices}", + "title": "Configure uma nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N\u00e3o ligue o dispositivo ao iniciar o Home Assistant" + }, + "description": "Definir as configura\u00e7\u00f5es gerais do dispositivo" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/pt.json b/homeassistant/components/arcam_fmj/translations/pt.json index fdeb639b12..097e3d086d 100644 --- a/homeassistant/components/arcam_fmj/translations/pt.json +++ b/homeassistant/components/arcam_fmj/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "error": { "one": "uma", "other": "mais" diff --git a/homeassistant/components/atag/translations/pt.json b/homeassistant/components/atag/translations/pt.json index d34bb36bc0..16752dd007 100644 --- a/homeassistant/components/atag/translations/pt.json +++ b/homeassistant/components/atag/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/august/translations/pt.json b/homeassistant/components/august/translations/pt.json index 5560383b71..45461329fc 100644 --- a/homeassistant/components/august/translations/pt.json +++ b/homeassistant/components/august/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/aurora/translations/pt.json b/homeassistant/components/aurora/translations/pt.json index aad75b3bed..336f6ac5f6 100644 --- a/homeassistant/components/aurora/translations/pt.json +++ b/homeassistant/components/aurora/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index a637b68b0b..baf9ce33cd 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -2,11 +2,16 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", + "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "Token de Acesso actualizado com sucesso" }, + "error": { + "invalid_access_token": "Token de acesso inv\u00e1lido" + }, "step": { "reauth": { "data": { + "access_token": "Token de Acesso", "email": "Email" } }, diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index b7cbb547b0..2175486703 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -5,7 +5,9 @@ "link_local_address": "Eendere\u00e7os de liga\u00e7\u00e3o local n\u00e3o s\u00e3o suportados" }, "error": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index 50d5409ef8..2af1f54844 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Token de Acesso atualizado com sucesso" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index cedcdc2732..9d7fdda100 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -98,6 +98,10 @@ "off": "Normal", "on": "Baixo" }, + "battery_charging": { + "off": "Sem carregar", + "on": "A carregar" + }, "cold": { "off": "Normal", "on": "Frio" @@ -122,6 +126,10 @@ "off": "Normal", "on": "Quente" }, + "light": { + "off": "Sem luz", + "on": "Com luz" + }, "lock": { "off": "Trancada", "on": "Destrancada" @@ -134,6 +142,10 @@ "off": "Limpo", "on": "Detectado" }, + "moving": { + "off": "Parado", + "on": "Em movimento" + }, "occupancy": { "off": "Limpo", "on": "Detectado" @@ -142,6 +154,10 @@ "off": "Fechado", "on": "Aberto" }, + "plug": { + "off": "Desligado", + "on": "Ligado" + }, "presence": { "off": "Fora", "on": "Casa" diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index b7fc26165a..5a8ebbeea0 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "error": { + "unknown": "Erro inesperado", "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." }, "step": { diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index 188effb27a..ed650faa02 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -10,7 +10,8 @@ }, "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index 9d37ff831d..5e5f1367f5 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -10,6 +10,9 @@ }, "step": { "authorize": { + "data": { + "pin": "C\u00f3digo PIN" + }, "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\nSe o c\u00f3digo PIN n\u00e3o for exibido, \u00e9 necess\u00e1rio cancelar o registro do Home Assistant na TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", "title": "Autorizar TV Sony Bravia" }, diff --git a/homeassistant/components/broadlink/translations/pt.json b/homeassistant/components/broadlink/translations/pt.json index bf246b55b3..45fb03ad04 100644 --- a/homeassistant/components/broadlink/translations/pt.json +++ b/homeassistant/components/broadlink/translations/pt.json @@ -2,11 +2,14 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unknown": "Erro inesperado" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unknown": "Erro inesperado" }, "flow_title": "{name} ({model} em {host})", diff --git a/homeassistant/components/brother/translations/pt.json b/homeassistant/components/brother/translations/pt.json index 5e4c740d66..d62513ec25 100644 --- a/homeassistant/components/brother/translations/pt.json +++ b/homeassistant/components/brother/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "wrong_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." }, "step": { diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index f681da4210..47b39d574b 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { "host": "Servidor", - "port": "Porta" + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/canary/translations/pt.json b/homeassistant/components/canary/translations/pt.json new file mode 100644 index 0000000000..e328e4f580 --- /dev/null +++ b/homeassistant/components/canary/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/pt.json b/homeassistant/components/cert_expiry/translations/pt.json index af42481b25..0e2e7eeb9c 100644 --- a/homeassistant/components/cert_expiry/translations/pt.json +++ b/homeassistant/components/cert_expiry/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "error": { + "connection_timeout": "Tempo excedido a tentar ligar ao servidor.", "resolve_failed": "N\u00e3o \u00e9 possivel resolver o servidor" }, "step": { @@ -11,5 +12,6 @@ } } } - } + }, + "title": "Validade do Certificado" } \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json new file mode 100644 index 0000000000..158cd3f3f7 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_token": "API Token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/pt.json b/homeassistant/components/coolmaster/translations/pt.json index ce7cbc3f54..f13cad90ed 100644 --- a/homeassistant/components/coolmaster/translations/pt.json +++ b/homeassistant/components/coolmaster/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 617aed245e..dd9b538ae8 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -4,9 +4,15 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { + "api_key": "API Key", "host": "Servidor", "password": "Palavra-passe" }, diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index ce6d6df890..b33812fa24 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "Bridge j\u00e1 est\u00e1 configurada", - "no_bridges": "Nenhum hub deCONZ descoberto" + "no_bridges": "Nenhum hub deCONZ descoberto", + "not_deconz_bridge": "N\u00e3o \u00e9 uma bridge deCONZ" }, "error": { - "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + "no_key": "N\u00e3o foi poss\u00edvel obter uma API Key" }, "step": { "link": { @@ -48,7 +49,8 @@ "remote_awakened": "Dispositivo acordou", "remote_button_double_press": "Bot\u00e3o \"{subtype}\" clicado duas vezes", "remote_button_long_press": "Bot\u00e3o \"{subtype}\" pressionado continuamente", - "remote_falling": "Dispositivo em queda livre" + "remote_falling": "Dispositivo em queda livre", + "remote_gyro_activated": "Dispositivo agitado" } } } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/pt.json b/homeassistant/components/denonavr/translations/pt.json index 34a23569b9..4a43bae51b 100644 --- a/homeassistant/components/denonavr/translations/pt.json +++ b/homeassistant/components/denonavr/translations/pt.json @@ -12,5 +12,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "zone2": "Configurar a Zona 2", + "zone3": "Configurar a Zona 3" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index b8a454fbab..ca6b9a6542 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "home_control_url": "Home Control [VOID]", + "mydevolo_url": "mydevolo [VOID]", + "password": "Palavra-passe", + "username": "Email / devolo ID" } } } diff --git a/homeassistant/components/dexcom/translations/pt.json b/homeassistant/components/dexcom/translations/pt.json index af953a1caa..8af2ff4345 100644 --- a/homeassistant/components/dexcom/translations/pt.json +++ b/homeassistant/components/dexcom/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/dialogflow/translations/pt.json b/homeassistant/components/dialogflow/translations/pt.json index 09ab0e6711..56c91431f1 100644 --- a/homeassistant/components/dialogflow/translations/pt.json +++ b/homeassistant/components/dialogflow/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar o [Dialogflow Webhook] ({dialogflow_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json\n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/doorbird/translations/pt.json b/homeassistant/components/doorbird/translations/pt.json index 3f200f4109..db021f4afc 100644 --- a/homeassistant/components/doorbird/translations/pt.json +++ b/homeassistant/components/doorbird/translations/pt.json @@ -1,11 +1,16 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { "host": "Servidor", "name": "Nome do dispositivo", - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/dunehd/translations/pt.json b/homeassistant/components/dunehd/translations/pt.json index ce7cbc3f54..188c4a2a6d 100644 --- a/homeassistant/components/dunehd/translations/pt.json +++ b/homeassistant/components/dunehd/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/ecobee/translations/pt.json b/homeassistant/components/ecobee/translations/pt.json index 20bba0ede4..f6e4d5f5dc 100644 --- a/homeassistant/components/ecobee/translations/pt.json +++ b/homeassistant/components/ecobee/translations/pt.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { "user": { "data": { "api_key": "Chave da API" - } + }, + "title": "ecobee API Key" } } } diff --git a/homeassistant/components/elgato/translations/pt.json b/homeassistant/components/elgato/translations/pt.json index c4d1cc35cc..99a658c7a0 100644 --- a/homeassistant/components/elgato/translations/pt.json +++ b/homeassistant/components/elgato/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/emulated_roku/translations/pt.json b/homeassistant/components/emulated_roku/translations/pt.json index 8c9b894c4b..479685ff7e 100644 --- a/homeassistant/components/emulated_roku/translations/pt.json +++ b/homeassistant/components/emulated_roku/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/pt.json b/homeassistant/components/epson/translations/pt.json new file mode 100644 index 0000000000..352e98916f --- /dev/null +++ b/homeassistant/components/epson/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index e010af99a0..6ff4d78644 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "O ESP j\u00e1 est\u00e1 configurado" + "already_configured": "O ESP j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" }, "error": { "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/pt.json b/homeassistant/components/fireservicerota/translations/pt.json new file mode 100644 index 0000000000..c78c9a5aba --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/pt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/pt.json b/homeassistant/components/flick_electric/translations/pt.json index 1e3d9138c8..4a071063d4 100644 --- a/homeassistant/components/flick_electric/translations/pt.json +++ b/homeassistant/components/flick_electric/translations/pt.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/flunearyou/translations/pt.json b/homeassistant/components/flunearyou/translations/pt.json index c7081cd694..83724bb7d0 100644 --- a/homeassistant/components/flunearyou/translations/pt.json +++ b/homeassistant/components/flunearyou/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/pt.json b/homeassistant/components/forked_daapd/translations/pt.json index 8d3dfe38d4..3ad6eaad7e 100644 --- a/homeassistant/components/forked_daapd/translations/pt.json +++ b/homeassistant/components/forked_daapd/translations/pt.json @@ -7,8 +7,11 @@ "user": { "data": { "host": "Servidor", - "password": "Palavra-passe da API (deixar em branco se sem palavra-passe)" - } + "name": "Nome amig\u00e1vel", + "password": "Palavra-passe da API (deixar em branco se sem palavra-passe)", + "port": "Porta da API" + }, + "title": "Configurar dispositivo forked-daapd" } } } diff --git a/homeassistant/components/freebox/translations/pt.json b/homeassistant/components/freebox/translations/pt.json index 09e13bc200..3cd69cf5dd 100644 --- a/homeassistant/components/freebox/translations/pt.json +++ b/homeassistant/components/freebox/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Servidor j\u00e1 configurado" }, + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/pt.json b/homeassistant/components/fritzbox/translations/pt.json index a5b5cd26dc..9d2eadee61 100644 --- a/homeassistant/components/fritzbox/translations/pt.json +++ b/homeassistant/components/fritzbox/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "confirm": { "data": { diff --git a/homeassistant/components/geofency/translations/pt.json b/homeassistant/components/geofency/translations/pt.json index 4e20283462..11e5023bae 100644 --- a/homeassistant/components/geofency/translations/pt.json +++ b/homeassistant/components/geofency/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Geofency. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/geonetnz_volcano/translations/pt.json b/homeassistant/components/geonetnz_volcano/translations/pt.json index 98180e1124..88f4021b4a 100644 --- a/homeassistant/components/geonetnz_volcano/translations/pt.json +++ b/homeassistant/components/geonetnz_volcano/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gios/translations/pt.json b/homeassistant/components/gios/translations/pt.json new file mode 100644 index 0000000000..286cd58dd8 --- /dev/null +++ b/homeassistant/components/gios/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/pt.json b/homeassistant/components/glances/translations/pt.json index f7195cd0bf..d5f64d59f9 100644 --- a/homeassistant/components/glances/translations/pt.json +++ b/homeassistant/components/glances/translations/pt.json @@ -10,9 +10,12 @@ "user": { "data": { "host": "Servidor", + "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/goalzero/translations/pt.json b/homeassistant/components/goalzero/translations/pt.json new file mode 100644 index 0000000000..5ce246347c --- /dev/null +++ b/homeassistant/components/goalzero/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/pt.json b/homeassistant/components/gpslogger/translations/pt.json index 8602afcabf..47e4e6e383 100644 --- a/homeassistant/components/gpslogger/translations/pt.json +++ b/homeassistant/components/gpslogger/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no GPslogger. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/gree/translations/pt.json b/homeassistant/components/gree/translations/pt.json new file mode 100644 index 0000000000..e25888655a --- /dev/null +++ b/homeassistant/components/gree/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt.json b/homeassistant/components/guardian/translations/pt.json index 0077ceddd4..b2fce54d6b 100644 --- a/homeassistant/components/guardian/translations/pt.json +++ b/homeassistant/components/guardian/translations/pt.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { + "ip_address": "Endere\u00e7o IP", "port": "Porta" } } diff --git a/homeassistant/components/harmony/translations/pt.json b/homeassistant/components/harmony/translations/pt.json index 2a9c91681b..04374af8e8 100644 --- a/homeassistant/components/harmony/translations/pt.json +++ b/homeassistant/components/harmony/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json index 973601e744..06083bae75 100644 --- a/homeassistant/components/hassio/translations/pt.json +++ b/homeassistant/components/hassio/translations/pt.json @@ -1,10 +1,13 @@ { "system_health": { "info": { + "board": "Tabela", "disk_total": "Disco Total", - "disk_used": "Disco Usado", + "disk_used": "Disco Utilizado", "docker_version": "Vers\u00e3o Docker", + "healthy": "Saud\u00e1vel", "host_os": "Sistema operativo anfitri\u00e3o", + "installed_addons": "Add-ons instalados", "supervisor_api": "API do Supervisor", "supervisor_version": "Vers\u00e3o do Supervisor", "supported": "Suportado" diff --git a/homeassistant/components/heos/translations/pt.json b/homeassistant/components/heos/translations/pt.json index ce7cbc3f54..a893104829 100644 --- a/homeassistant/components/heos/translations/pt.json +++ b/homeassistant/components/heos/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/home_connect/translations/pt.json b/homeassistant/components/home_connect/translations/pt.json new file mode 100644 index 0000000000..462b746816 --- /dev/null +++ b/homeassistant/components/home_connect/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index 7bf340567f..c16c2c3baa 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -2,9 +2,10 @@ "system_health": { "info": { "arch": "Arquitetura do Processador", + "chassis": "Chassis", "dev": "Desenvolvimento", - "docker": "Docker", - "docker_version": "Docker", + "docker": "", + "docker_version": "", "hassio": "Supervisor", "host_os": "Sistema Operativo do Home Assistant", "installation_type": "Tipo de Instala\u00e7\u00e3o", diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json new file mode 100644 index 0000000000..b5da3fdfc9 --- /dev/null +++ b/homeassistant/components/homekit/translations/pt.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "pairing": { + "title": "Emparelhar HomeKit" + }, + "user": { + "data": { + "include_domains": "Dom\u00ednios a incluir" + }, + "title": "Activar o HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "title": "Configura\u00e7\u00e3o avan\u00e7ada" + }, + "cameras": { + "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." + }, + "include_exclude": { + "data": { + "entities": "Entidades", + "mode": "Modo" + }, + "title": "Selecione as entidades a serem expostas" + }, + "init": { + "data": { + "include_domains": "Dom\u00ednios a incluir", + "mode": "Modo" + }, + "title": "Selecione os dom\u00ednios a serem expostos." + }, + "yaml": { + "description": "Esta entrada \u00e9 controlada via YAML", + "title": "Ajustar as op\u00e7\u00f5es do HomeKit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index deeb376e5e..c4ab5c1e63 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -18,6 +18,9 @@ }, "flow_title": "Acess\u00f3rio HomeKit: {name}", "step": { + "busy_error": { + "title": "O dispositivo j\u00e1 est\u00e1 a emparelhar com outro controlador" + }, "pair": { "data": { "pairing_code": "C\u00f3digo de emparelhamento" @@ -34,5 +37,22 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Bot\u00e3o 1", + "button10": "Bot\u00e3o 10", + "button2": "Bot\u00e3o 2", + "button3": "Bot\u00e3o 3", + "button4": "Bot\u00e3o 4", + "button5": "Bot\u00e3o 5", + "button6": "Bot\u00e3o 6", + "button7": "Bot\u00e3o 7", + "button8": "Bot\u00e3o 8", + "button9": "Bot\u00e3o 9" + }, + "trigger_type": { + "single_press": "\"{subtype}\" pressionado" + } + }, "title": "Acess\u00f3rio HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index 81a8804ee4..34d057a9ab 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -7,7 +7,9 @@ "error": { "connection_timeout": "Liga\u00e7\u00e3o expirou", "incorrect_password": "Palavra-passe incorreta", - "incorrect_username": "Nome de Utilizador incorreto" + "incorrect_username": "Nome de Utilizador incorreto", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "flow_title": "Huawei LTE: {name}", "step": { diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index eef0cc82c1..f573f0645b 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -6,6 +6,7 @@ "cannot_connect": "N\u00e3o foi poss\u00edvel conectar-se ao hub", "discover_timeout": "Nenhum hub Hue descoberto", "no_bridges": "Nenhum hub Philips Hue descoberto", + "not_hue_bridge": "N\u00e3o \u00e9 uma bridge Hue", "unknown": "Ocorreu um erro desconhecido" }, "error": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pt.json b/homeassistant/components/hunterdouglas_powerview/translations/pt.json index 8b7889f0d1..ef5279e090 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pt.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/hyperion/translations/pt.json b/homeassistant/components/hyperion/translations/pt.json new file mode 100644 index 0000000000..ac9710c6b9 --- /dev/null +++ b/homeassistant/components/hyperion/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/pt.json b/homeassistant/components/iaqualink/translations/pt.json index 24825307e7..3b46686633 100644 --- a/homeassistant/components/iaqualink/translations/pt.json +++ b/homeassistant/components/iaqualink/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index 420196bb05..3a3a13b91c 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -1,6 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { + "reauth": { + "data": { + "password": "Palavra-passe" + }, + "description": "A sua palavra-passe anteriormente introduzida para {username} j\u00e1 n\u00e3o \u00e9 v\u00e1lida. Atualize sua palavra-passe para continuar a utilizar esta integra\u00e7\u00e3o.", + "title": "Reautenticar integra\u00e7\u00e3o" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" diff --git a/homeassistant/components/ifttt/translations/pt.json b/homeassistant/components/ifttt/translations/pt.json index eaed455b71..030af8e090 100644 --- a/homeassistant/components/ifttt/translations/pt.json +++ b/homeassistant/components/ifttt/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, precisa de utilizar a a\u00e7\u00e3o \"Make a web request\" no [IFTTT Webhook applet]({applet_url}).\n\nPreencha com a seguinte informa\u00e7\u00e3o:\n\n- URL: `{webhook_url}`\n- Method: POST \n- Content Type: application/json \n\nConsulte [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para lidar com dados de entrada." }, diff --git a/homeassistant/components/ipma/translations/pt.json b/homeassistant/components/ipma/translations/pt.json index 3f25486c6a..a9ebd3c23e 100644 --- a/homeassistant/components/ipma/translations/pt.json +++ b/homeassistant/components/ipma/translations/pt.json @@ -15,5 +15,10 @@ "title": "Localiza\u00e7\u00e3o" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Servidor API do IPMA dispon\u00edvel" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/pt.json b/homeassistant/components/ipp/translations/pt.json index 02353e5fca..eccf48139b 100644 --- a/homeassistant/components/ipp/translations/pt.json +++ b/homeassistant/components/ipp/translations/pt.json @@ -1,14 +1,20 @@ { "config": { "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "ipp_error": "Erro IPP encontrado.", "ipp_version_error": "Vers\u00e3o IPP n\u00e3o suportada pela impressora." }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { "host": "Servidor", - "port": "Porta" + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/iqvia/translations/pt.json b/homeassistant/components/iqvia/translations/pt.json new file mode 100644 index 0000000000..d252c078a2 --- /dev/null +++ b/homeassistant/components/iqvia/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/pt.json b/homeassistant/components/islamic_prayer_times/translations/pt.json new file mode 100644 index 0000000000..25538aa003 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index b8a454fbab..fe44091317 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "password": "Palavra-passe" + "host": "", + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/juicenet/translations/pt.json b/homeassistant/components/juicenet/translations/pt.json index 0c5c776056..db82206819 100644 --- a/homeassistant/components/juicenet/translations/pt.json +++ b/homeassistant/components/juicenet/translations/pt.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/cs.json b/homeassistant/components/kodi/translations/cs.json index ccfb08328f..157c61cf24 100644 --- a/homeassistant/components/kodi/translations/cs.json +++ b/homeassistant/components/kodi/translations/cs.json @@ -36,7 +36,8 @@ "ws_port": { "data": { "ws_port": "Port" - } + }, + "description": "Port WebSocket (n\u011bkdy se v Kodi naz\u00fdv\u00e1 port TCP). Abyste se mohli p\u0159ipojit p\u0159es WebSocket, mus\u00edte povolit \"Povolit programy ... ovl\u00e1dat Kodi\" v Syst\u00e9m / Nastaven\u00ed / S\u00ed\u0165 / Slu\u017eby. Pokud WebSocket nen\u00ed povolen, odeberte port a nechte pr\u00e1zdn\u00e9." } } }, diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index 972aed55cc..19ddbf6057 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -8,5 +8,44 @@ } } } + }, + "options": { + "step": { + "options_binary": { + "data": { + "name": "Nome (opcional)" + } + }, + "options_digital": { + "data": { + "name": "Nome (opcional)" + } + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7" + } + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9" + } + }, + "options_switch": { + "data": { + "name": "Nome (opcional)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/pt.json b/homeassistant/components/kulersky/translations/pt.json new file mode 100644 index 0000000000..e25888655a --- /dev/null +++ b/homeassistant/components/kulersky/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index 9c848bd8ec..71370e4006 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "error": { - "invalid_username": "Nome de utilizador incorreto" + "already_configured": "Conta j\u00e1 configurada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_username": "Nome de utilizador incorreto", + "unknown": "Erro inesperado" }, "step": { "user": { diff --git a/homeassistant/components/local_ip/translations/pt.json b/homeassistant/components/local_ip/translations/pt.json new file mode 100644 index 0000000000..ef31de296c --- /dev/null +++ b/homeassistant/components/local_ip/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/pt.json b/homeassistant/components/locative/translations/pt.json index 6ca0b0b194..9357506812 100644 --- a/homeassistant/components/locative/translations/pt.json +++ b/homeassistant/components/locative/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Locative. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/logi_circle/translations/pt.json b/homeassistant/components/logi_circle/translations/pt.json new file mode 100644 index 0000000000..9a0e75c24f --- /dev/null +++ b/homeassistant/components/logi_circle/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "error": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/pt.json b/homeassistant/components/luftdaten/translations/pt.json index 1f8cb29ff0..5811494fff 100644 --- a/homeassistant/components/luftdaten/translations/pt.json +++ b/homeassistant/components/luftdaten/translations/pt.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_sensor": "Sensor n\u00e3o dispon\u00edvel ou inv\u00e1lido" }, "step": { diff --git a/homeassistant/components/mailgun/translations/pt.json b/homeassistant/components/mailgun/translations/pt.json index 2a193a19f9..614256fc70 100644 --- a/homeassistant/components/mailgun/translations/pt.json +++ b/homeassistant/components/mailgun/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar [Webhooks with Mailgun]({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json\n\n Veja [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, diff --git a/homeassistant/components/met/translations/pt.json b/homeassistant/components/met/translations/pt.json index 6641658bd4..2cfc8af2d6 100644 --- a/homeassistant/components/met/translations/pt.json +++ b/homeassistant/components/met/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/metoffice/translations/pt.json b/homeassistant/components/metoffice/translations/pt.json new file mode 100644 index 0000000000..7a8b164e32 --- /dev/null +++ b/homeassistant/components/metoffice/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/pt.json b/homeassistant/components/mikrotik/translations/pt.json index 77ce7025f7..b177591d4e 100644 --- a/homeassistant/components/mikrotik/translations/pt.json +++ b/homeassistant/components/mikrotik/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { "host": "Servidor", + "name": "Nome", "password": "Palavra-passe", "port": "Porta", "username": "Nome de Utilizador" diff --git a/homeassistant/components/mill/translations/pt.json b/homeassistant/components/mill/translations/pt.json index b8a454fbab..f1e1a2f9fe 100644 --- a/homeassistant/components/mill/translations/pt.json +++ b/homeassistant/components/mill/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/mobile_app/translations/pt.json b/homeassistant/components/mobile_app/translations/pt.json index bfef7be3f1..9ca512ca53 100644 --- a/homeassistant/components/mobile_app/translations/pt.json +++ b/homeassistant/components/mobile_app/translations/pt.json @@ -8,5 +8,10 @@ "description": "Deseja configurar o componente Mobile App?" } } + }, + "device_automation": { + "action_type": { + "notify": "Enviar uma notifica\u00e7\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json new file mode 100644 index 0000000000..fe188057e4 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "connection_error": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "host": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index 2f0e67a977..325e8dde09 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -63,7 +63,11 @@ "options": { "data": { "birth_enable": "Povolit zpr\u00e1vu p\u0159i p\u0159ipojen\u00ed", - "discovery": "Povolit zji\u0161\u0165ov\u00e1n\u00ed" + "discovery": "Povolit zji\u0161\u0165ov\u00e1n\u00ed", + "will_payload": "Obsah zpr\u00e1vy se z\u00e1v\u011bt\u00ed", + "will_qos": "QoS zpr\u00e1vy se z\u00e1v\u011bt\u00ed", + "will_retain": "Zachov\u00e1n\u00ed zpr\u00e1vy se z\u00e1v\u011bt\u00ed", + "will_topic": "T\u00e9ma zpr\u00e1vy se z\u00e1v\u011bt\u00ed (will message)" }, "description": "Zvolte mo\u017enosti MQTT." } diff --git a/homeassistant/components/myq/translations/pt.json b/homeassistant/components/myq/translations/pt.json index 4a071063d4..b47060ac87 100644 --- a/homeassistant/components/myq/translations/pt.json +++ b/homeassistant/components/myq/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index b464235997..809cbc360a 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 79c4276c23..6da647ac29 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -2,10 +2,18 @@ "config": { "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o." + "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" }, "error": { "internal_error": "Erro interno ao validar o c\u00f3digo", + "invalid_pin": "C\u00f3digo PIN inv\u00e1lido", "timeout": "Limite temporal ultrapassado ao validar c\u00f3digo", "unknown": "Erro desconhecido ao validar o c\u00f3digo" }, @@ -23,6 +31,9 @@ }, "description": "Para associar \u00e0 sua conta Nest, [autorizar a sua conta]({url}).\n\nAp\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo pin fornecido abaixo.", "title": "Associar conta Nest" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } } diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index f9199091a8..afab5e616e 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -2,10 +2,16 @@ "config": { "abort": { "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "create_entry": { "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } } }, "options": { diff --git a/homeassistant/components/nexia/translations/pt.json b/homeassistant/components/nexia/translations/pt.json index 4a071063d4..7953cf5625 100644 --- a/homeassistant/components/nexia/translations/pt.json +++ b/homeassistant/components/nexia/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json index 657ce03e54..f2766f5a2c 100644 --- a/homeassistant/components/nightscout/translations/pt.json +++ b/homeassistant/components/nightscout/translations/pt.json @@ -6,6 +6,14 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "url": "" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/pt.json b/homeassistant/components/notion/translations/pt.json index 24825307e7..c0a3dae8aa 100644 --- a/homeassistant/components/notion/translations/pt.json +++ b/homeassistant/components/notion/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/omnilogic/translations/pt.json b/homeassistant/components/omnilogic/translations/pt.json new file mode 100644 index 0000000000..3e10b97777 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json new file mode 100644 index 0000000000..bd1e14729e --- /dev/null +++ b/homeassistant/components/onewire/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "owserver": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index cfc92a512d..c3662a032a 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -7,6 +7,9 @@ "no_mac": "N\u00e3o foi poss\u00edvel configurar o ID unico para o dispositivo ONVIF.", "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json index 960e3a9cf5..4285ee45c8 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/openweathermap/translations/pt.json b/homeassistant/components/openweathermap/translations/pt.json index f736ec7e3c..aabac4f4cf 100644 --- a/homeassistant/components/openweathermap/translations/pt.json +++ b/homeassistant/components/openweathermap/translations/pt.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { + "api_key": "API Key", "language": "Idioma", "latitude": "Latitude", "longitude": "Longitude", diff --git a/homeassistant/components/ovo_energy/translations/pt.json b/homeassistant/components/ovo_energy/translations/pt.json index 3fbf1797b3..7015a44b5f 100644 --- a/homeassistant/components/ovo_energy/translations/pt.json +++ b/homeassistant/components/ovo_energy/translations/pt.json @@ -1,9 +1,16 @@ { "config": { "error": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/owntracks/translations/pt.json b/homeassistant/components/owntracks/translations/pt.json index dbc8db55d6..ebc78e1346 100644 --- a/homeassistant/components/owntracks/translations/pt.json +++ b/homeassistant/components/owntracks/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "create_entry": { "default": "\n\n No Android, abra [o aplicativo OwnTracks] ( {android_url} ), v\u00e1 para prefer\u00eancias - > conex\u00e3o. Altere as seguintes configura\u00e7\u00f5es: \n - Modo: HTTP privado \n - Anfitri\u00e3o: {webhook_url} \n - Identifica\u00e7\u00e3o: \n - Nome de usu\u00e1rio: ` \n - ID do dispositivo: ` ` \n\n No iOS, abra [o aplicativo OwnTracks] ( {ios_url} ), toque no \u00edcone (i) no canto superior esquerdo - > configura\u00e7\u00f5es. Altere as seguintes configura\u00e7\u00f5es: \n - Modo: HTTP \n - URL: {webhook_url} \n - Ativar autentica\u00e7\u00e3o \n - UserID: ` ` \n\n {secret} \n \n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais informa\u00e7\u00f5es." }, diff --git a/homeassistant/components/ozw/translations/pt.json b/homeassistant/components/ozw/translations/pt.json new file mode 100644 index 0000000000..75d8509787 --- /dev/null +++ b/homeassistant/components/ozw/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "start_addon": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/pt.json b/homeassistant/components/panasonic_viera/translations/pt.json index 1e4f4cadc2..9a5c19acf1 100644 --- a/homeassistant/components/panasonic_viera/translations/pt.json +++ b/homeassistant/components/panasonic_viera/translations/pt.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_pin_code": "O C\u00f3digo PIN digitado \u00e9 inv\u00e1lido" + }, "step": { "pairing": { "data": { "pin": "PIN" }, + "description": "Digite o C\u00f3digo PIN exibido na sua TV", "title": "Emparelhamento" }, "user": { @@ -12,6 +21,7 @@ "host": "Endere\u00e7o IP", "name": "Nome" }, + "description": "Introduza o Endere\u00e7o IP da sua TV Panasonic Viera", "title": "Configure a sua TV" } } diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index f681da4210..6e0b2481c8 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -3,8 +3,13 @@ "step": { "user": { "data": { + "api_key": "API Key", "host": "Servidor", - "port": "Porta" + "location": "Localiza\u00e7\u00e3o", + "name": "Nome", + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json new file mode 100644 index 0000000000..3d0630027a --- /dev/null +++ b/homeassistant/components/plaato/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index 81b70bcd08..3b63ab169e 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", "already_in_progress": "Plex est\u00e1 a ser configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Falha por motivo desconhecido" }, "error": { @@ -12,7 +13,9 @@ "manual_setup": { "data": { "host": "Servidor", - "port": "Porta" + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } }, "select_server": { diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json index 808e3f3f7e..65283f66f4 100644 --- a/homeassistant/components/plugwise/translations/pt.json +++ b/homeassistant/components/plugwise/translations/pt.json @@ -1,7 +1,17 @@ { "config": { "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user_gateway": { + "data": { + "host": "Endere\u00e7o IP", + "port": "Porta" + } + } } }, "options": { diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 1ccfdddc1d..401e10c256 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", - "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/)." + "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { "default": "Autenticado com sucesso com Minut para o(s) seu(s) dispositivo (s) Point" diff --git a/homeassistant/components/powerwall/translations/pt.json b/homeassistant/components/powerwall/translations/pt.json index 0c5c776056..c748619963 100644 --- a/homeassistant/components/powerwall/translations/pt.json +++ b/homeassistant/components/powerwall/translations/pt.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/pt.json b/homeassistant/components/profiler/translations/pt.json new file mode 100644 index 0000000000..c299020ce9 --- /dev/null +++ b/homeassistant/components/profiler/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/pt.json b/homeassistant/components/progettihwsw/translations/pt.json index 82e6756df1..072d1ea056 100644 --- a/homeassistant/components/progettihwsw/translations/pt.json +++ b/homeassistant/components/progettihwsw/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index a8b6c3c6cc..51ea84f7c2 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "credential_error": "Erro ao obter credenciais.", "no_devices_found": "N\u00e3o foram encontrados dispositivos PlayStation 4 na rede." }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "login_failed": "Falha ao emparelhar com a PlayStation 4. Verifique se o PIN est\u00e1 correto." }, "step": { diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json index 4c01137c49..a5648a9138 100644 --- a/homeassistant/components/rachio/translations/pt.json +++ b/homeassistant/components/rachio/translations/pt.json @@ -1,12 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "api_key": "Chave de API" + "api_key": "API Key" } } } diff --git a/homeassistant/components/rainmachine/translations/pt.json b/homeassistant/components/rainmachine/translations/pt.json index e6c1baf1ca..99cd4dba4e 100644 --- a/homeassistant/components/rainmachine/translations/pt.json +++ b/homeassistant/components/rainmachine/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/recollect_waste/translations/pt.json b/homeassistant/components/recollect_waste/translations/pt.json index 57e7ea502f..31e872a71c 100644 --- a/homeassistant/components/recollect_waste/translations/pt.json +++ b/homeassistant/components/recollect_waste/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rfxtrx/translations/pt.json b/homeassistant/components/rfxtrx/translations/pt.json index ce8a928727..335e097e99 100644 --- a/homeassistant/components/rfxtrx/translations/pt.json +++ b/homeassistant/components/rfxtrx/translations/pt.json @@ -2,6 +2,28 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "setup_network": { + "data": { + "host": "Servidor", + "port": "Porta" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "Caminho do Dispositivo USB" + } + } + } + }, + "options": { + "error": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/pt.json b/homeassistant/components/rpi_power/translations/pt.json new file mode 100644 index 0000000000..9890048c36 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/pt.json b/homeassistant/components/ruckus_unleashed/translations/pt.json new file mode 100644 index 0000000000..561c8d7728 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index 5ce246347c..8493f66031 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sentry/translations/cs.json b/homeassistant/components/sentry/translations/cs.json index ad96aeba53..28a2d603dd 100644 --- a/homeassistant/components/sentry/translations/cs.json +++ b/homeassistant/components/sentry/translations/cs.json @@ -22,7 +22,8 @@ "init": { "data": { "environment": "Voliteln\u00e9 jm\u00e9no prost\u0159ed\u00ed.", - "tracing": "Povolit sledov\u00e1n\u00ed v\u00fdkonu" + "tracing": "Povolit sledov\u00e1n\u00ed v\u00fdkonu", + "tracing_sample_rate": "Vzorkovac\u00ed frekvence trasov\u00e1n\u00ed; mezi 0.0 a 1.0 (1.0 = 100 %)" } } } diff --git a/homeassistant/components/sharkiq/translations/pt.json b/homeassistant/components/sharkiq/translations/pt.json index 565b9f6c0e..dfae15e968 100644 --- a/homeassistant/components/sharkiq/translations/pt.json +++ b/homeassistant/components/sharkiq/translations/pt.json @@ -1,11 +1,23 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "unknown": "Erro inesperado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "reauth": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json index b02332eb0b..d66cc0e5dd 100644 --- a/homeassistant/components/shelly/translations/pt.json +++ b/homeassistant/components/shelly/translations/pt.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "flow_title": "Shelly: {name}", @@ -12,6 +13,12 @@ "confirm_discovery": { "description": "Deseja configurar o {model} em {host} ?" }, + "credentials": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/simplisafe/translations/pt.json b/homeassistant/components/simplisafe/translations/pt.json index a208b49150..9b5df6cf93 100644 --- a/homeassistant/components/simplisafe/translations/pt.json +++ b/homeassistant/components/simplisafe/translations/pt.json @@ -1,14 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { "identifier_exists": "Conta j\u00e1 registada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "reauth_confirm": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/smappee/translations/pt.json b/homeassistant/components/smappee/translations/pt.json index aba871acf6..54f841b224 100644 --- a/homeassistant/components/smappee/translations/pt.json +++ b/homeassistant/components/smappee/translations/pt.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" }, "step": { "local": { "data": { "host": "Servidor" } + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } } diff --git a/homeassistant/components/smarthab/translations/pt.json b/homeassistant/components/smarthab/translations/pt.json index 2933743c86..7430480cc0 100644 --- a/homeassistant/components/smarthab/translations/pt.json +++ b/homeassistant/components/smarthab/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smartthings/translations/pt.json b/homeassistant/components/smartthings/translations/pt.json index efab29fd69..9f2ed5a4b9 100644 --- a/homeassistant/components/smartthings/translations/pt.json +++ b/homeassistant/components/smartthings/translations/pt.json @@ -15,6 +15,9 @@ "title": "Autorizar o Home Assistant" }, "pat": { + "data": { + "access_token": "Token de Acesso" + }, "title": "Insira o Token de acesso pessoal" }, "select_location": { diff --git a/homeassistant/components/solaredge/translations/pt.json b/homeassistant/components/solaredge/translations/pt.json index 01078bbddf..52bbd75e9b 100644 --- a/homeassistant/components/solaredge/translations/pt.json +++ b/homeassistant/components/solaredge/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { - "api_key": "Chave de API" + "api_key": "API Key" } } } diff --git a/homeassistant/components/somfy/translations/pt.json b/homeassistant/components/somfy/translations/pt.json new file mode 100644 index 0000000000..28b7a920e9 --- /dev/null +++ b/homeassistant/components/somfy/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/pt.json b/homeassistant/components/sonarr/translations/pt.json index ce7cbc3f54..43b4c5d49b 100644 --- a/homeassistant/components/sonarr/translations/pt.json +++ b/homeassistant/components/sonarr/translations/pt.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { - "host": "Servidor" + "api_key": "API Key", + "host": "Servidor", + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/songpal/translations/pt.json b/homeassistant/components/songpal/translations/pt.json new file mode 100644 index 0000000000..ce8a928727 --- /dev/null +++ b/homeassistant/components/songpal/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/pt.json b/homeassistant/components/speedtestdotnet/translations/pt.json new file mode 100644 index 0000000000..c299020ce9 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/pt.json b/homeassistant/components/spotify/translations/pt.json index b459d4e6bf..f3da93ad96 100644 --- a/homeassistant/components/spotify/translations/pt.json +++ b/homeassistant/components/spotify/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "reauth_account_mismatch": "A conta Spotify com a qual foi autenticada n\u00e3o corresponde \u00e0 conta necess\u00e1ria para a reautentica\u00e7\u00e3o." }, "step": { diff --git a/homeassistant/components/srp_energy/translations/pt.json b/homeassistant/components/srp_energy/translations/pt.json new file mode 100644 index 0000000000..3e10b97777 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/pt.json b/homeassistant/components/synology_dsm/translations/pt.json index 4264b1e4a0..dcc4132df7 100644 --- a/homeassistant/components/synology_dsm/translations/pt.json +++ b/homeassistant/components/synology_dsm/translations/pt.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "2sa": { "data": { @@ -10,7 +15,9 @@ "data": { "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" } }, "user": { @@ -18,7 +25,9 @@ "host": "Servidor", "password": "Palavra-passe", "port": "Porta (opcional)", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/tasmota/translations/pt.json b/homeassistant/components/tasmota/translations/pt.json new file mode 100644 index 0000000000..3df19f11fa --- /dev/null +++ b/homeassistant/components/tasmota/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_discovery_topic": "Prefixo do t\u00f3pico para descoberta inv\u00e1lido." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "Prefixo do t\u00f3pico para descoberta" + }, + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index 6030c97244..e8abc7c865 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -3,7 +3,11 @@ "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", - "unknown": "Ocorreu um erro desconhecido" + "unknown": "Ocorreu um erro desconhecido", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "auth": { diff --git a/homeassistant/components/tesla/translations/pt.json b/homeassistant/components/tesla/translations/pt.json index 0df67a9418..c249c325ad 100644 --- a/homeassistant/components/tesla/translations/pt.json +++ b/homeassistant/components/tesla/translations/pt.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tibber/translations/pt.json b/homeassistant/components/tibber/translations/pt.json index 23f4662a4c..bed3f76be0 100644 --- a/homeassistant/components/tibber/translations/pt.json +++ b/homeassistant/components/tibber/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tile/translations/pt.json b/homeassistant/components/tile/translations/pt.json index e266cf0626..e7e5968a6d 100644 --- a/homeassistant/components/tile/translations/pt.json +++ b/homeassistant/components/tile/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/toon/translations/pt.json b/homeassistant/components/toon/translations/pt.json index 9ecaef216f..189f0c1b2f 100644 --- a/homeassistant/components/toon/translations/pt.json +++ b/homeassistant/components/toon/translations/pt.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "step": { "agreement": { diff --git a/homeassistant/components/totalconnect/translations/pt.json b/homeassistant/components/totalconnect/translations/pt.json index d399370847..3c17682089 100644 --- a/homeassistant/components/totalconnect/translations/pt.json +++ b/homeassistant/components/totalconnect/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Conta j\u00e1 configurada" }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/traccar/translations/pt.json b/homeassistant/components/traccar/translations/pt.json new file mode 100644 index 0000000000..3d0630027a --- /dev/null +++ b/homeassistant/components/traccar/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index a68c763550..c311dbf9ef 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "name_exists": "Nome j\u00e1 existe" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tuya/translations/pt.json b/homeassistant/components/tuya/translations/pt.json index b8a454fbab..ae7ec8fa5e 100644 --- a/homeassistant/components/tuya/translations/pt.json +++ b/homeassistant/components/tuya/translations/pt.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/twentemilieu/translations/pt.json b/homeassistant/components/twentemilieu/translations/pt.json new file mode 100644 index 0000000000..451ff82e74 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/pt.json b/homeassistant/components/twilio/translations/pt.json index a5a1d76bfb..997757d2bc 100644 --- a/homeassistant/components/twilio/translations/pt.json +++ b/homeassistant/components/twilio/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar [Webhooks with Twilio] ({twilio_url}). \n\nPreencha as seguintes informa\u00e7\u00f5es: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST \n- Tipo de Conte\u00fado: application/x-www-form-urlencoded \n\nVeja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, diff --git a/homeassistant/components/twinkly/translations/pt.json b/homeassistant/components/twinkly/translations/pt.json new file mode 100644 index 0000000000..abed97c893 --- /dev/null +++ b/homeassistant/components/twinkly/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "device_exists": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json index 0c5c776056..ae100e4584 100644 --- a/homeassistant/components/upb/translations/pt.json +++ b/homeassistant/components/upb/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "unknown": "Erro inesperado" } diff --git a/homeassistant/components/upcloud/translations/pt.json b/homeassistant/components/upcloud/translations/pt.json new file mode 100644 index 0000000000..a2f3208768 --- /dev/null +++ b/homeassistant/components/upcloud/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/pt.json b/homeassistant/components/vesync/translations/pt.json index 5cf1a0dcd0..fb4e459281 100644 --- a/homeassistant/components/vesync/translations/pt.json +++ b/homeassistant/components/vesync/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vilfo/translations/pt.json b/homeassistant/components/vilfo/translations/pt.json index ce7cbc3f54..c30760a9a8 100644 --- a/homeassistant/components/vilfo/translations/pt.json +++ b/homeassistant/components/vilfo/translations/pt.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "access_token": "Token de Acesso", "host": "Servidor" } } diff --git a/homeassistant/components/vizio/translations/pt.json b/homeassistant/components/vizio/translations/pt.json index b1a4f0d7b3..dce6f5934d 100644 --- a/homeassistant/components/vizio/translations/pt.json +++ b/homeassistant/components/vizio/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "pair_tv": { "data": { diff --git a/homeassistant/components/water_heater/translations/pt.json b/homeassistant/components/water_heater/translations/pt.json new file mode 100644 index 0000000000..2278e7701a --- /dev/null +++ b/homeassistant/components/water_heater/translations/pt.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/pt.json b/homeassistant/components/wiffi/translations/pt.json new file mode 100644 index 0000000000..0077ceddd4 --- /dev/null +++ b/homeassistant/components/wiffi/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index b80d6630c3..83caf877b9 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "error": { + "already_configured": "Conta j\u00e1 configurada" + }, "step": { "profile": { "data": { diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index a6e5cd46cb..849cb1588f 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wolflink/translations/pt.json b/homeassistant/components/wolflink/translations/pt.json index 7953cf5625..308f60400a 100644 --- a/homeassistant/components/wolflink/translations/pt.json +++ b/homeassistant/components/wolflink/translations/pt.json @@ -9,6 +9,11 @@ "unknown": "Erro inesperado" }, "step": { + "device": { + "data": { + "device_name": "Dispositivo" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/xbox/translations/pt.json b/homeassistant/components/xbox/translations/pt.json new file mode 100644 index 0000000000..38f070ab3d --- /dev/null +++ b/homeassistant/components/xbox/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index 5c127b797e..cd1c9b7978 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "gateway": { "data": { - "host": "Endere\u00e7o IP" + "host": "Endere\u00e7o IP", + "token": "API Token" } } } diff --git a/homeassistant/components/yeelight/translations/cs.json b/homeassistant/components/yeelight/translations/cs.json index 4ede775331..8bab9bd19b 100644 --- a/homeassistant/components/yeelight/translations/cs.json +++ b/homeassistant/components/yeelight/translations/cs.json @@ -26,8 +26,10 @@ "init": { "data": { "model": "Model (voliteln\u00fd)", + "nightlight_switch": "Pou\u017e\u00edt p\u0159ep\u00edna\u010d no\u010dn\u00edho osv\u011btlen\u00ed", "save_on_change": "Ulo\u017eit stav p\u0159i zm\u011bn\u011b", - "transition": "\u010cas p\u0159echodu (v ms)" + "transition": "\u010cas p\u0159echodu (v ms)", + "use_music_mode": "Povolit hudebn\u00ed re\u017eim" }, "description": "Pokud ponech\u00e1te model pr\u00e1zdn\u00fd, bude automaticky rozpozn\u00e1n." } diff --git a/homeassistant/components/yeelight/translations/pt.json b/homeassistant/components/yeelight/translations/pt.json index e4a8cc8062..6d35018898 100644 --- a/homeassistant/components/yeelight/translations/pt.json +++ b/homeassistant/components/yeelight/translations/pt.json @@ -14,6 +14,9 @@ } }, "user": { + "data": { + "host": "Servidor" + }, "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." } } diff --git a/homeassistant/components/zerproc/translations/pt.json b/homeassistant/components/zerproc/translations/pt.json new file mode 100644 index 0000000000..1424e35f35 --- /dev/null +++ b/homeassistant/components/zerproc/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/pt.json b/homeassistant/components/zoneminder/translations/pt.json new file mode 100644 index 0000000000..f8fa0efe96 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pt.json b/homeassistant/components/zwave/translations/pt.json index 49be02c195..7494208188 100644 --- a/homeassistant/components/zwave/translations/pt.json +++ b/homeassistant/components/zwave/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado" + "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o dispositivo USB est\u00e1 correto?" From 30f73a49621f92b132e01502b3d38dba20319a70 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Sun, 6 Dec 2020 20:09:32 -0500 Subject: [PATCH 039/302] Add discovery of sensors on DS2409 MicroLan (#43599) * Add discovery of sensors on DS2409 MicroLan * Add tests for coupler * Update tests * Fix isort * Clean up * Move tests to test_sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .../components/onewire/onewirehub.py | 30 +++-- .../components/onewire/test_binary_sensor.py | 3 + .../onewire/test_entity_owserver.py | 15 +++ tests/components/onewire/test_sensor.py | 126 +++++++++++++++++- tests/components/onewire/test_switch.py | 3 + 5 files changed, 161 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 3b715eed0d..09a3235377 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -11,6 +11,11 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS +DEVICE_COUPLERS = { + # Family : [branches] + "1F": ["aux", "main"] +} + class OneWireHub: """Hub to communicate with SysBus or OWServer.""" @@ -62,17 +67,24 @@ class OneWireHub: ) return self.devices - def _discover_devices_owserver(self): + def _discover_devices_owserver(self, path="/"): """Discover all owserver devices.""" devices = [] - for device_path in self.owproxy.dir(): - devices.append( - { - "path": device_path, - "family": self.owproxy.read(f"{device_path}family").decode(), - "type": self.owproxy.read(f"{device_path}type").decode(), - } - ) + for device_path in self.owproxy.dir(path): + device_family = self.owproxy.read(f"{device_path}family").decode() + device_type = self.owproxy.read(f"{device_path}type").decode() + device_branches = DEVICE_COUPLERS.get(device_family) + if device_branches: + for branch in device_branches: + devices += self._discover_devices_owserver(f"{device_path}{branch}") + else: + devices.append( + { + "path": device_path, + "family": device_family, + "type": device_type, + } + ) return devices diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index bc8cf1defc..be740b13fb 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -84,3 +84,6 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): assert registry_entry is not None state = hass.states.get(entity_id) assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor.get( + "device_file", registry_entry.unique_id + ) diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/test_entity_owserver.py index a09808316c..aee84f9fe2 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/test_entity_owserver.py @@ -179,6 +179,18 @@ MOCK_DEVICE_SENSORS = { }, ], }, + "1F.111111111111": { + "inject_reads": [ + b"DS2409", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "1F.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2409", + "name": "1F.111111111111", + }, + SENSOR_DOMAIN: [], + }, "22.111111111111": { "inject_reads": [ b"DS1822", # read device type @@ -752,3 +764,6 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): assert state is None else: assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor.get( + "device_file", registry_entry.unique_id + ) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 751ef10614..ad9580f34e 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,16 +1,66 @@ """Tests for 1-Wire sensor platform.""" -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR -import homeassistant.components.sensor as sensor +from pyownet.protocol import Error as ProtocolError +import pytest + +from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component +from . import setup_onewire_patched_owserver_integration + +from tests.async_mock import patch +from tests.common import assert_setup_component, mock_registry + +MOCK_COUPLERS = { + "1F.111111111111": { + "inject_reads": [ + b"DS2409", # read device type + ], + "branches": { + "aux": {}, + "main": { + "1D.111111111111": { + "inject_reads": [ + b"DS2423", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "1D.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2423", + "name": "1D.111111111111", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.1d_111111111111_counter_a", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", + "unique_id": "/1D.111111111111/counter.A", + "injected_value": b" 251123", + "result": "251123", + "unit": "count", + "class": None, + }, + { + "entity_id": "sensor.1d_111111111111_counter_b", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", + "unique_id": "/1D.111111111111/counter.B", + "injected_value": b" 248125", + "result": "248125", + "unit": "count", + "class": None, + }, + ], + }, + }, + }, + } +} async def test_setup_minimum(hass): """Test old platform setup with minimum configuration.""" config = {"sensor": {"platform": "onewire"}} with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() @@ -23,7 +73,7 @@ async def test_setup_sysbus(hass): } } with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() @@ -31,7 +81,7 @@ async def test_setup_owserver(hass): """Test old platform setup with OWServer configuration.""" config = {"sensor": {"platform": "onewire", "host": "localhost"}} with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() @@ -39,5 +89,67 @@ async def test_setup_owserver_with_port(hass): """Test old platform setup with OWServer configuration.""" config = {"sensor": {"platform": "onewire", "host": "localhost", "port": "1234"}} with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() + + +@pytest.mark.parametrize("device_id", ["1F.111111111111"]) +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): + """Test for 1-Wire sensors connected to DS2409 coupler.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + + mock_coupler = MOCK_COUPLERS[device_id] + + dir_side_effect = [] # List of lists of string + read_side_effect = [] # List of byte arrays + + dir_side_effect.append([f"/{device_id}/"]) # dir on root + read_side_effect.append(device_id[0:2].encode()) # read family on root + if "inject_reads" in mock_coupler: + read_side_effect += mock_coupler["inject_reads"] + + expected_sensors = [] + for branch, branch_details in mock_coupler["branches"].items(): + dir_side_effect.append( + [ # dir on branch + f"/{device_id}/{branch}/{sub_device_id}/" + for sub_device_id in branch_details + ] + ) + + for sub_device_id, sub_device in branch_details.items(): + read_side_effect.append(sub_device_id[0:2].encode()) + if "inject_reads" in sub_device: + read_side_effect.extend(sub_device["inject_reads"]) + + expected_sensors += sub_device[SENSOR_DOMAIN] + for expected_sensor in sub_device[SENSOR_DOMAIN]: + read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect.extend([ProtocolError("Missing injected value")] * 10) + owproxy.return_value.dir.side_effect = dir_side_effect + owproxy.return_value.read.side_effect = read_side_effect + + with patch("homeassistant.components.onewire.SUPPORTED_PLATFORMS", [SENSOR_DOMAIN]): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_sensors) + + for expected_sensor in expected_sensors: + entity_id = expected_sensor["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_sensor["unique_id"] + assert registry_entry.unit_of_measurement == expected_sensor["unit"] + assert registry_entry.device_class == expected_sensor["class"] + assert registry_entry.disabled == expected_sensor.get("disabled", False) + state = hass.states.get(entity_id) + if registry_entry.disabled: + assert state is None + else: + assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor["device_file"] diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 3a1f2eb9f7..0c70ad3c9f 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -127,3 +127,6 @@ async def test_owserver_switch(owproxy, hass, device_id): state = hass.states.get(entity_id) assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor.get( + "device_file", registry_entry.unique_id + ) From 18736dbebfe2588307f2eb4a38d9be6ee95c41ff Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 7 Dec 2020 02:49:36 +0100 Subject: [PATCH 040/302] Bump voluptuous to 0.12.1 (#44002) * Bump voluptuous to 0.12.1 * Adjust MQTT climate test to new error msg --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- tests/components/mqtt/test_climate.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d820f5e715..1818385c0e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ requests==2.25.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 -voluptuous==0.12.0 +voluptuous==0.12.1 yarl==1.4.2 zeroconf==0.28.6 diff --git a/requirements.txt b/requirements.txt index ece1877ea7..cbe339fd83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,6 @@ pytz>=2020.1 pyyaml==5.3.1 requests==2.25.0 ruamel.yaml==0.15.100 -voluptuous==0.12.0 +voluptuous==0.12.1 voluptuous-serialize==2.4.0 yarl==1.4.2 diff --git a/setup.py b/setup.py index d5d133d4a3..c9acb4d82d 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ REQUIRES = [ "pyyaml==5.3.1", "requests==2.25.0", "ruamel.yaml==0.15.100", - "voluptuous==0.12.0", + "voluptuous==0.12.1", "voluptuous-serialize==2.4.0", "yarl==1.4.2", ] diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 4d049753f4..0a9c1dc610 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -133,9 +133,9 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" with pytest.raises(vol.Invalid) as excinfo: await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) - assert ("value is not allowed for dictionary value @ data['hvac_mode']") in str( - excinfo.value - ) + assert ( + "value must be one of ['auto', 'cool', 'dry', 'fan_only', 'heat', 'heat_cool', 'off'] for dictionary value @ data['hvac_mode']" + ) in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" From 25e717c8d26f0f125318658d08ad5d16eb11d3ff Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 7 Dec 2020 03:16:43 +0100 Subject: [PATCH 041/302] Bump fritzconnection to 1.4.0 (#43996) --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index a1924296f7..45b73cf58e 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,6 +2,6 @@ "domain": "fritz", "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.3.4"], + "requirements": ["fritzconnection==1.4.0"], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index e06d3b881f..4879842ee2 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "fritzbox_callmonitor", "name": "AVM FRITZ!Box Call Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.3.4"], + "requirements": ["fritzconnection==1.4.0"], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 3eeac8bd8d..d2fe23a811 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "fritzbox_netmonitor", "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==1.3.4"], + "requirements": ["fritzconnection==1.4.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e599f62c2..65092ac608 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,7 +619,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -fritzconnection==1.3.4 +fritzconnection==1.4.0 # homeassistant.components.google_translate gTTS==2.2.1 From 19389b16e27b18273478c870bb1f9205f64cc2d1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 7 Dec 2020 03:50:22 +0100 Subject: [PATCH 042/302] Add support for system health to GIOS integration (#43280) * Add system health support * Fix docstrings * Change url to check --- homeassistant/components/gios/strings.json | 5 +++ .../components/gios/system_health.py | 20 ++++++++++ tests/components/gios/test_system_health.py | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 homeassistant/components/gios/system_health.py create mode 100644 tests/components/gios/test_system_health.py diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json index ce80aa8786..ef2fec9f84 100644 --- a/homeassistant/components/gios/strings.json +++ b/homeassistant/components/gios/strings.json @@ -18,5 +18,10 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "system_health": { + "info": { + "can_reach_server": "Reach GIO\u015a server" + } } } diff --git a/homeassistant/components/gios/system_health.py b/homeassistant/components/gios/system_health.py new file mode 100644 index 0000000000..391a8c1aff --- /dev/null +++ b/homeassistant/components/gios/system_health.py @@ -0,0 +1,20 @@ +"""Provide info to system health.""" +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +API_ENDPOINT = "http://api.gios.gov.pl/" + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, API_ENDPOINT) + } diff --git a/tests/components/gios/test_system_health.py b/tests/components/gios/test_system_health.py new file mode 100644 index 0000000000..c58b8b12b5 --- /dev/null +++ b/tests/components/gios/test_system_health.py @@ -0,0 +1,39 @@ +"""Test GIOS system health.""" +import asyncio + +from aiohttp import ClientError + +from homeassistant.components.gios.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info + + +async def test_gios_system_health(hass, aioclient_mock): + """Test GIOS system health.""" + aioclient_mock.get("http://api.gios.gov.pl/", text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": "ok"} + + +async def test_gios_system_health_fail(hass, aioclient_mock): + """Test GIOS system health.""" + aioclient_mock.get("http://api.gios.gov.pl/", exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}} From 906e1ce9609ea9aad45a47ea76b137a2fca706a0 Mon Sep 17 00:00:00 2001 From: nivnoach Date: Mon, 7 Dec 2020 10:25:04 +0200 Subject: [PATCH 043/302] Allow manual configuration of ignored config entries (#43947) --- homeassistant/config_entries.py | 3 ++ tests/components/shelly/test_config_flow.py | 32 ++++++++++++++- tests/test_config_entries.py | 45 +++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index af82db0ffb..c42a53b2da 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -911,6 +911,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) + # Allow ignored entries to be configured on manual user step + if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER: + continue raise data_entry_flow.AbortFlow("already_configured") async def async_set_unique_id( diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1796847bd7..75ac015b83 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -5,7 +5,7 @@ import aiohttp import aioshelly import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.shelly.const import DOMAIN from tests.async_mock import AsyncMock, Mock, patch @@ -231,6 +231,36 @@ async def test_form_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_user_setup_ignored_device(hass): + """Test user can successfully setup an ignored device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="shelly", + unique_id="test-mac", + data={"host": "0.0.0.0"}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + async def test_form_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 59e1b0754c..0be89af481 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1539,6 +1539,51 @@ async def test_unique_id_ignore(hass, manager): assert entry.unique_id == "mock-unique-id" +async def test_manual_add_overrides_ignored_entry(hass, manager): + """Test that we can ignore manually add entry, overriding ignored entry.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": "0.0.0.0"}, + unique_id="mock-unique-id", + state=config_entries.ENTRY_STATE_LOADED, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule("comp"), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + await self.async_set_unique_id("mock-unique-id") + self._abort_if_unique_id_configured( + updates={"host": "1.1.1.1"}, reload_on_update=False + ) + return self.async_show_form(step_id="step2") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert entry.data["host"] == "1.1.1.1" + assert entry.data["additional"] == "data" + assert len(async_reload.mock_calls) == 0 + + async def test_unignore_step_form(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" async_setup_entry = AsyncMock(return_value=True) From 5fc7df2746aaee683e8e247597ff95fd46b03eb2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 7 Dec 2020 10:27:33 +0200 Subject: [PATCH 044/302] Prevent firing Shelly input events at startup (#43986) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 3c34da574a..298c7e111b 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -158,7 +158,11 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): last_event_count = self._last_input_events_count.get(channel) self._last_input_events_count[channel] = block.inputEventCnt - if last_event_count == block.inputEventCnt or event_type == "": + if ( + last_event_count is None + or last_event_count == block.inputEventCnt + or event_type == "" + ): continue if event_type in INPUTS_EVENTS_DICT: From 4bcb6c0ec696596f8b79d4aea218525d709204cc Mon Sep 17 00:00:00 2001 From: mbo18 Date: Mon, 7 Dec 2020 10:47:29 +0100 Subject: [PATCH 045/302] Add UV unit to meteo_france UV sensor (#43992) --- homeassistant/components/meteo_france/const.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index bfbaa828ea..0055d32393 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -26,6 +26,7 @@ from homeassistant.const import ( PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, + UV_INDEX, ) DOMAIN = "meteo_france" @@ -110,7 +111,7 @@ SENSOR_TYPES = { }, "uv": { ENTITY_NAME: "UV", - ENTITY_UNIT: None, + ENTITY_UNIT: UV_INDEX, ENTITY_ICON: "mdi:sunglasses", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, From 035860ebb2f68e566867d30f421e2eea4bb31143 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 7 Dec 2020 11:20:22 +0100 Subject: [PATCH 046/302] Fix Solaredge integration in case the data is not complete (#43557) Co-authored-by: Martin Hjelmare --- homeassistant/components/solaredge/sensor.py | 29 ++++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 0cd498b4e3..e3e59676bf 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -425,22 +425,21 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): self.data = {} self.attributes = {} self.unit = energy_details["unit"] - meters = energy_details["meters"] - for entity in meters: - for key, data in entity.items(): - if key == "type" and data in [ - "Production", - "SelfConsumption", - "FeedIn", - "Purchased", - "Consumption", - ]: - energy_type = data - if key == "values": - for row in data: - self.data[energy_type] = row["value"] - self.attributes[energy_type] = {"date": row["date"]} + for meter in energy_details["meters"]: + if "type" not in meter or "values" not in meter: + continue + if meter["type"] not in [ + "Production", + "SelfConsumption", + "FeedIn", + "Purchased", + "Consumption", + ]: + continue + if len(meter["values"][0]) == 2: + self.data[meter["type"]] = meter["values"][0]["value"] + self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} _LOGGER.debug( "Updated SolarEdge energy details: %s, %s", self.data, self.attributes From 84973ec8f795be0553e5112867f7eb54ecf6154b Mon Sep 17 00:00:00 2001 From: JJdeVries <43748187+JJdeVries@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:46:53 +0100 Subject: [PATCH 047/302] Fix unit of measurement for asuswrt sensors (#44009) --- homeassistant/components/asuswrt/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 15ca58a525..aa13bee81d 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -29,7 +29,7 @@ class _SensorTypes(enum.Enum): UPLOAD_SPEED = "upload_speed" @property - def unit(self) -> Optional[str]: + def unit_of_measurement(self) -> Optional[str]: """Return a string with the unit of the sensortype.""" if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD): return DATA_GIGABYTES @@ -161,3 +161,8 @@ class AsuswrtSensor(CoordinatorEntity): def icon(self) -> Optional[str]: """Return the icon to use in the frontend.""" return self._type.icon + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + return self._type.unit_of_measurement From fec623fb4edfc8e04c74285b800b3834265ad4d6 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Mon, 7 Dec 2020 13:13:41 +0100 Subject: [PATCH 048/302] Fix LCN service calls (invoking coroutines) (#43932) Co-authored-by: Martin Hjelmare --- homeassistant/components/lcn/__init__.py | 2 +- homeassistant/components/lcn/services.py | 164 ++++++++++++----------- 2 files changed, 88 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index fa18c0e578..72f11b7b00 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -117,7 +117,7 @@ async def async_setup(hass, config): ("pck", Pck), ): hass.services.async_register( - DOMAIN, service_name, service(hass), service.schema + DOMAIN, service_name, service(hass).async_call_service, service.schema ) return True diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index baa318f891..d7d8acf4f2 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -1,4 +1,5 @@ """Service calls related dependencies for LCN component.""" + import pypck import voluptuous as vol @@ -54,11 +55,12 @@ class LcnServiceCall: def __init__(self, hass): """Initialize service call.""" + self.hass = hass self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - def get_address_connection(self, call): - """Get address connection object.""" - addr, connection_id = call.data[CONF_ADDRESS] + def get_device_connection(self, service): + """Get device connection object.""" + addr, connection_id = service.data[CONF_ADDRESS] addr = pypck.lcn_addr.LcnAddr(*addr) if connection_id is None: connection = self.connections[0] @@ -67,6 +69,10 @@ class LcnServiceCall: return connection.get_address_conn(addr) + async def async_call_service(self, service): + """Execute service call.""" + raise NotImplementedError + class OutputAbs(LcnServiceCall): """Set absolute brightness of output port in percent.""" @@ -83,16 +89,16 @@ class OutputAbs(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] - brightness = call.data[CONF_BRIGHTNESS] + output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] + brightness = service.data[CONF_BRIGHTNESS] transition = pypck.lcn_defs.time_to_ramp_value( - call.data[CONF_TRANSITION] * 1000 + service.data[CONF_TRANSITION] * 1000 ) - address_connection = self.get_address_connection(call) - address_connection.dim_output(output.value, brightness, transition) + device_connection = self.get_device_connection(service) + await device_connection.dim_output(output.value, brightness, transition) class OutputRel(LcnServiceCall): @@ -107,13 +113,13 @@ class OutputRel(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] - brightness = call.data[CONF_BRIGHTNESS] + output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] + brightness = service.data[CONF_BRIGHTNESS] - address_connection = self.get_address_connection(call) - address_connection.rel_output(output.value, brightness) + device_connection = self.get_device_connection(service) + await device_connection.rel_output(output.value, brightness) class OutputToggle(LcnServiceCall): @@ -128,15 +134,15 @@ class OutputToggle(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] + output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] transition = pypck.lcn_defs.time_to_ramp_value( - call.data[CONF_TRANSITION] * 1000 + service.data[CONF_TRANSITION] * 1000 ) - address_connection = self.get_address_connection(call) - address_connection.toggle_output(output.value, transition) + device_connection = self.get_device_connection(service) + await device_connection.toggle_output(output.value, transition) class Relays(LcnServiceCall): @@ -146,14 +152,15 @@ class Relays(LcnServiceCall): {vol.Required(CONF_STATE): is_relays_states_string} ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" states = [ - pypck.lcn_defs.RelayStateModifier[state] for state in call.data[CONF_STATE] + pypck.lcn_defs.RelayStateModifier[state] + for state in service.data[CONF_STATE] ] - address_connection = self.get_address_connection(call) - address_connection.control_relays(states) + device_connection = self.get_device_connection(service) + await device_connection.control_relays(states) class Led(LcnServiceCall): @@ -166,13 +173,13 @@ class Led(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - led = pypck.lcn_defs.LedPort[call.data[CONF_LED]] - led_state = pypck.lcn_defs.LedStatus[call.data[CONF_STATE]] + led = pypck.lcn_defs.LedPort[service.data[CONF_LED]] + led_state = pypck.lcn_defs.LedStatus[service.data[CONF_STATE]] - address_connection = self.get_address_connection(call) - address_connection.control_led(led, led_state) + device_connection = self.get_device_connection(service) + await device_connection.control_led(led, led_state) class VarAbs(LcnServiceCall): @@ -194,14 +201,14 @@ class VarAbs(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] - value = call.data[CONF_VALUE] - unit = pypck.lcn_defs.VarUnit.parse(call.data[CONF_UNIT_OF_MEASUREMENT]) + var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] + value = service.data[CONF_VALUE] + unit = pypck.lcn_defs.VarUnit.parse(service.data[CONF_UNIT_OF_MEASUREMENT]) - address_connection = self.get_address_connection(call) - address_connection.var_abs(var, value, unit) + device_connection = self.get_device_connection(service) + await device_connection.var_abs(var, value, unit) class VarReset(LcnServiceCall): @@ -211,12 +218,12 @@ class VarReset(LcnServiceCall): {vol.Required(CONF_VARIABLE): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS))} ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] + var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] - address_connection = self.get_address_connection(call) - address_connection.var_reset(var) + device_connection = self.get_device_connection(service) + await device_connection.var_reset(var) class VarRel(LcnServiceCall): @@ -237,15 +244,15 @@ class VarRel(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] - value = call.data[CONF_VALUE] - unit = pypck.lcn_defs.VarUnit.parse(call.data[CONF_UNIT_OF_MEASUREMENT]) - value_ref = pypck.lcn_defs.RelVarRef[call.data[CONF_RELVARREF]] + var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] + value = service.data[CONF_VALUE] + unit = pypck.lcn_defs.VarUnit.parse(service.data[CONF_UNIT_OF_MEASUREMENT]) + value_ref = pypck.lcn_defs.RelVarRef[service.data[CONF_RELVARREF]] - address_connection = self.get_address_connection(call) - address_connection.var_rel(var, value, unit, value_ref) + device_connection = self.get_device_connection(service) + await device_connection.var_rel(var, value, unit, value_ref) class LockRegulator(LcnServiceCall): @@ -258,14 +265,14 @@ class LockRegulator(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - setpoint = pypck.lcn_defs.Var[call.data[CONF_SETPOINT]] - state = call.data[CONF_STATE] + setpoint = pypck.lcn_defs.Var[service.data[CONF_SETPOINT]] + state = service.data[CONF_STATE] reg_id = pypck.lcn_defs.Var.to_set_point_id(setpoint) - address_connection = self.get_address_connection(call) - address_connection.lock_regulator(reg_id, state) + device_connection = self.get_device_connection(service) + await device_connection.lock_regulator(reg_id, state) class SendKeys(LcnServiceCall): @@ -286,31 +293,31 @@ class SendKeys(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - address_connection = self.get_address_connection(call) + device_connection = self.get_device_connection(service) keys = [[False] * 8 for i in range(4)] - key_strings = zip(call.data[CONF_KEYS][::2], call.data[CONF_KEYS][1::2]) + key_strings = zip(service.data[CONF_KEYS][::2], service.data[CONF_KEYS][1::2]) for table, key in key_strings: table_id = ord(table) - 65 key_id = int(key) - 1 keys[table_id][key_id] = True - delay_time = call.data[CONF_TIME] + delay_time = service.data[CONF_TIME] if delay_time != 0: hit = pypck.lcn_defs.SendKeyCommand.HIT - if pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] != hit: + if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit: raise ValueError( "Only hit command is allowed when sending deferred keys." ) - delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) - address_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) + delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT]) + await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) else: - state = pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] - address_connection.send_keys(keys, state) + state = pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] + await device_connection.send_keys(keys, state) class LockKeys(LcnServiceCall): @@ -329,28 +336,31 @@ class LockKeys(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - address_connection = self.get_address_connection(call) + device_connection = self.get_device_connection(service) states = [ pypck.lcn_defs.KeyLockStateModifier[state] - for state in call.data[CONF_STATE] + for state in service.data[CONF_STATE] ] - table_id = ord(call.data[CONF_TABLE]) - 65 + table_id = ord(service.data[CONF_TABLE]) - 65 - delay_time = call.data[CONF_TIME] + delay_time = service.data[CONF_TIME] if delay_time != 0: if table_id != 0: raise ValueError( "Only table A is allowed when locking keys for a specific time." ) - delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) - address_connection.lock_keys_tab_a_temporary(delay_time, delay_unit, states) + delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT]) + await device_connection.lock_keys_tab_a_temporary( + delay_time, delay_unit, states + ) else: - address_connection.lock_keys(table_id, states) + await device_connection.lock_keys(table_id, states) - address_connection.request_status_locked_keys_timeout() + handler = device_connection.status_request_handler + await handler.request_status_locked_keys_timeout() class DynText(LcnServiceCall): @@ -363,13 +373,13 @@ class DynText(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - row_id = call.data[CONF_ROW] - 1 - text = call.data[CONF_TEXT] + row_id = service.data[CONF_ROW] - 1 + text = service.data[CONF_TEXT] - address_connection = self.get_address_connection(call) - address_connection.dyn_text(row_id, text) + device_connection = self.get_device_connection(service) + await device_connection.dyn_text(row_id, text) class Pck(LcnServiceCall): @@ -377,8 +387,8 @@ class Pck(LcnServiceCall): schema = LcnServiceCall.schema.extend({vol.Required(CONF_PCK): str}) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - pck = call.data[CONF_PCK] - address_connection = self.get_address_connection(call) - address_connection.pck(pck) + pck = service.data[CONF_PCK] + device_connection = self.get_device_connection(service) + await device_connection.pck(pck) From 1d0b4290feed28e91b9990aea80dc41571c532ce Mon Sep 17 00:00:00 2001 From: Nigel Rook Date: Mon, 7 Dec 2020 12:14:54 +0000 Subject: [PATCH 049/302] Update generic_thermostat current_temperature on startup (#43951) Co-authored-by: Martin Hjelmare --- .../components/generic_thermostat/climate.py | 10 +++++--- .../generic_thermostat/test_climate.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 4072c43bc2..175ee8f1d5 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, callback +from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -207,7 +207,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ) @callback - def _async_startup(event): + def _async_startup(*_): """Init on startup.""" sensor_state = self.hass.states.get(self.sensor_entity_id) if sensor_state and sensor_state.state not in ( @@ -215,8 +215,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity): STATE_UNKNOWN, ): self._async_update_temp(sensor_state) + self.async_write_ha_state() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + if self.hass.state == CoreState.running: + _async_startup() + else: + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index eaf7c8e565..71c6f41282 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -209,6 +209,30 @@ async def test_setup_defaults_to_unknown(hass): assert HVAC_MODE_OFF == hass.states.get(ENTITY).state +async def test_setup_gets_current_temp_from_sensor(hass): + """Test that current temperature is updated on entity addition.""" + hass.config.units = METRIC_SYSTEM + _setup_sensor(hass, 18) + await hass.async_block_till_done() + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_temp": 16, + } + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY).attributes["current_temperature"] == 18 + + async def test_default_setup_params(hass, setup_comp_2): """Test the setup with default parameters.""" state = hass.states.get(ENTITY) From 727b1d37b69c506371757e5165d57b859b690f3d Mon Sep 17 00:00:00 2001 From: PeteBa Date: Mon, 7 Dec 2020 12:16:56 +0000 Subject: [PATCH 050/302] Add discovery for MQTT device tracker (#42327) --- .../mqtt/device_tracker/__init__.py | 7 + .../mqtt/device_tracker/schema_discovery.py | 229 +++++++++++ .../schema_yaml.py} | 11 +- homeassistant/components/mqtt/discovery.py | 1 + .../mqtt/test_device_tracker_discovery.py | 361 ++++++++++++++++++ 5 files changed, 602 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/mqtt/device_tracker/__init__.py create mode 100644 homeassistant/components/mqtt/device_tracker/schema_discovery.py rename homeassistant/components/mqtt/{device_tracker.py => device_tracker/schema_yaml.py} (86%) create mode 100644 tests/components/mqtt/test_device_tracker_discovery.py diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py new file mode 100644 index 0000000000..03574e6554 --- /dev/null +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -0,0 +1,7 @@ +"""Support for tracking MQTT enabled devices.""" +from .schema_discovery import async_setup_entry_from_discovery +from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml + +PLATFORM_SCHEMA = PLATFORM_SCHEMA_YAML +async_setup_scanner = async_setup_scanner_from_yaml +async_setup_entry = async_setup_entry_from_discovery diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py new file mode 100644 index 0000000000..aa45f8f92b --- /dev/null +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -0,0 +1,229 @@ +"""Support for tracking MQTT enabled devices identified through discovery.""" +import logging + +import voluptuous as vol + +from homeassistant.components import device_tracker, mqtt +from homeassistant.components.device_tracker import SOURCE_TYPES +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import ( + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_DEVICE, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_VALUE_TEMPLATE, + STATE_HOME, + STATE_NOT_HOME, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .. import ( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC +from ..debug_info import log_messages +from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash + +_LOGGER = logging.getLogger(__name__) + +CONF_PAYLOAD_HOME = "payload_home" +CONF_PAYLOAD_NOT_HOME = "payload_not_home" +CONF_SOURCE_TYPE = "source_type" + +PLATFORM_SCHEMA_DISCOVERY = ( + mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) + .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) +) + + +async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): + """Set up MQTT device tracker dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add an MQTT device tracker.""" + discovery_data = discovery_payload.discovery_data + try: + config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) + await _async_setup_entity( + hass, config, async_add_entities, config_entry, discovery_data + ) + except Exception: + clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(device_tracker.DOMAIN, "mqtt"), async_discover + ) + + +async def _async_setup_entity( + hass, config, async_add_entities, config_entry=None, discovery_data=None +): + """Set up the MQTT Device Tracker entity.""" + async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) + + +class MqttDeviceTracker( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + TrackerEntity, +): + """Representation of a device tracker using MQTT.""" + + def __init__(self, hass, config, config_entry, discovery_data): + """Initialize the tracker.""" + self.hass = hass + self._location_name = None + self._sub_state = None + self._unique_id = config.get(CONF_UNIQUE_ID) + + # Load config + self._setup_from_config(config) + + device_config = config.get(CONF_DEVICE) + + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + + async def async_added_to_hass(self): + """Subscribe to MQTT events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) + self._setup_from_config(config) + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) + await self._subscribe_topics() + self.async_write_ha_state() + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._config = config + + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = self.hass + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg): + """Handle new MQTT messages.""" + payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + payload = value_template.async_render_with_possible_json_value(payload) + if payload == self._config[CONF_PAYLOAD_HOME]: + self._location_name = STATE_HOME + elif payload == self._config[CONF_PAYLOAD_NOT_HOME]: + self._location_name = STATE_NOT_HOME + else: + self._location_name = msg.payload + + self.async_write_ha_state() + + self._sub_state = await subscription.async_subscribe_topics( + self.hass, + self._sub_state, + { + "state_topic": { + "topic": self._config[CONF_STATE_TOPIC], + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + } + }, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state + ) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) + await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + + @property + def icon(self): + """Return the icon of the device.""" + return self._config.get(CONF_ICON) + + @property + def latitude(self): + """Return latitude if provided in device_state_attributes or None.""" + if ( + self.device_state_attributes is not None + and ATTR_LATITUDE in self.device_state_attributes + ): + return self.device_state_attributes[ATTR_LATITUDE] + return None + + @property + def location_accuracy(self): + """Return location accuracy if provided in device_state_attributes or None.""" + if ( + self.device_state_attributes is not None + and ATTR_GPS_ACCURACY in self.device_state_attributes + ): + return self.device_state_attributes[ATTR_GPS_ACCURACY] + return None + + @property + def longitude(self): + """Return longitude if provided in device_state_attributes or None.""" + if ( + self.device_state_attributes is not None + and ATTR_LONGITUDE in self.device_state_attributes + ): + return self.device_state_attributes[ATTR_LONGITUDE] + return None + + @property + def location_name(self): + """Return a location name for the current location of the device.""" + return self._location_name + + @property + def name(self): + """Return the name of the device tracker.""" + return self._config.get(CONF_NAME) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return self._config.get(CONF_SOURCE_TYPE) diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py similarity index 86% rename from homeassistant/components/mqtt/device_tracker.py rename to homeassistant/components/mqtt/device_tracker/schema_yaml.py index bcc969f035..520bced238 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,5 +1,4 @@ -"""Support for tracking MQTT enabled devices.""" -import logging +"""Support for tracking MQTT enabled devices defined in YAML.""" import voluptuous as vol @@ -9,15 +8,13 @@ from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from . import CONF_QOS - -_LOGGER = logging.getLogger(__name__) +from ..const import CONF_QOS CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( { vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -27,7 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( ) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d1e64d44bb..ca576f83d2 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -34,6 +34,7 @@ SUPPORTED_COMPONENTS = [ "climate", "cover", "device_automation", + "device_tracker", "fan", "light", "lock", diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py new file mode 100644 index 0000000000..4ee6986e59 --- /dev/null +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -0,0 +1,361 @@ +"""The tests for the MQTT device_tracker discovery platform.""" + +import pytest + +from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN + +from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_discover_device_tracker(hass, mqtt_mock, caplog): + """Test discovering an MQTT device tracker component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test_topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state is not None + assert state.name == "test" + assert ("device_tracker", "bla") in hass.data[ALREADY_DISCOVERED] + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is None + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "required-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Beer" + + +async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): + """Test for a non duplicate component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + state_duplicate = hass.states.get("device_tracker.beer1") + + assert state is not None + assert state.name == "Beer" + assert state_duplicate is None + assert "Component has already been discovered: device_tracker bla" in caplog.text + + +async def test_device_tracker_removal(hass, mqtt_mock, caplog): + """Test removal of component through empty discovery message.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is None + + +async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): + """Test rediscover of removed component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is None + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + +async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): + """Test for a non duplicate component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + assert "Component has already been discovered: device_tracker bla" in caplog.text + caplog.clear() + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + + assert ( + "Component has already been discovered: device_tracker bla" not in caplog.text + ) + + +async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): + """Test for a discovery update event.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Beer" + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Cider", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Cider" + + +async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): + """Test discvered device is cleaned up when removed from registry.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/tracker",' + ' "unique_id": "unique" }', + ) + await hass.async_block_till_done() + + # Verify device and registry entries are created + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is not None + entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") + assert entity_entry is not None + + state = hass.states.get("device_tracker.mqtt_unique") + assert state is not None + + device_reg.async_remove_device(device_entry.id) + await hass.async_block_till_done() + + # Verify device and registry entries are cleared + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is None + entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") + assert entity_entry is None + + # Verify state is removed + state = hass.states.get("device_tracker.mqtt_unique") + assert state is None + await hass.async_block_till_done() + + # Verify retained discovery topic has been cleared + mqtt_mock.async_publish.assert_called_once_with( + "homeassistant/device_tracker/bla/config", "", 0, True + ) + + +async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, caplog): + """Test the setting of the value via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test-topic" }', + ) + + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "not_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_value_via_mqtt_message_and_template( + hass, mqtt_mock, caplog +): + """Test the setting of the value via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{" + '"name": "test", ' + '"state_topic": "test-topic", ' + '"value_template": "{% if value is equalto \\"proxy_for_home\\" %}home{% else %}not_home{% endif %}" ' + "}", + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", "proxy_for_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "anything_for_not_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_value_via_mqtt_message_and_template2( + hass, mqtt_mock, caplog +): + """Test the setting of the value via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{" + '"name": "test", ' + '"state_topic": "test-topic", ' + '"value_template": "{{ value | lower }}" ' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "HOME") + state = hass.states.get("device_Tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "NOT_HOME") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_location_via_mqtt_message( + hass, mqtt_mock, caplog +): + """Test the setting of the location via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "test-location") + state = hass.states.get("device_tracker.test") + assert state.state == "test-location" + + +async def test_setting_device_tracker_location_via_lat_lon_message( + hass, mqtt_mock, caplog +): + """Test the setting of the latitude and longitude via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{ " + '"name": "test", ' + '"state_topic": "test-topic", ' + '"json_attributes_topic": "attributes-topic" ' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + hass.config.latitude = 32.87336 + hass.config.longitude = -117.22743 + + async_fire_mqtt_message( + hass, + "attributes-topic", + '{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}', + ) + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 32.87336 + assert state.attributes["longitude"] == -117.22743 + assert state.attributes["gps_accuracy"] == 1.5 + assert state.state == STATE_HOME + + async_fire_mqtt_message( + hass, + "attributes-topic", + '{"latitude":50.1,"longitude": -2.1, "gps_accuracy":1.5}', + ) + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 50.1 + assert state.attributes["longitude"] == -2.1 + assert state.attributes["gps_accuracy"] == 1.5 + assert state.state == STATE_NOT_HOME + + async_fire_mqtt_message(hass, "attributes-topic", '{"longitude": -117.22743}') + state = hass.states.get("device_tracker.test") + assert state.attributes["longitude"] == -117.22743 + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "attributes-topic", '{"latitude":32.87336}') + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 32.87336 + assert state.state == STATE_NOT_HOME From 6ce45e39d1621dc3e5848709173c2e69654e9dfc Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 7 Dec 2020 12:51:35 +0000 Subject: [PATCH 051/302] Hide HomeKit devices from discovery that are known to be problematic (#44014) --- .../components/homekit_controller/config_flow.py | 12 ++++++++++++ .../homekit_controller/test_config_flow.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9881ef15dc..71c8005cbc 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -21,6 +21,14 @@ HOMEKIT_BRIDGE_DOMAIN = "homekit" HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" +HOMEKIT_IGNORE = [ + # eufy Indoor Cam 2K Pan & Tilt + # https://github.com/home-assistant/core/issues/42307 + "T8410", + # Hive Hub - vendor does not give user a pairing code + "HHKBridge1,1", +] + PAIRING_FILE = "pairing.json" MDNS_SUFFIX = "._hap._tcp.local." @@ -255,6 +263,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. + if model in HOMEKIT_IGNORE: + return self.async_abort(reason="ignored_model") + + # If this is a HomeKit bridge exported by *this* HA instance ignore it. if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a8eb869abf..72a8133159 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -257,6 +257,21 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info["properties"]["md"] = "HHKBridge1,1" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + +async def test_discovery_ignored_hk_bridge(hass, controller): + """Already paired.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") From c9c3a8fe38f66465818194857ae26a8772123568 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 7 Dec 2020 05:10:46 -0800 Subject: [PATCH 052/302] Unregister updates when a Wemo entry is removed (#44005) --- homeassistant/components/wemo/binary_sensor.py | 5 +++++ homeassistant/components/wemo/fan.py | 5 +++++ homeassistant/components/wemo/light.py | 5 +++++ homeassistant/components/wemo/switch.py | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index b5ef3dc528..44031e846c 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -67,6 +67,11 @@ class WemoBinarySensor(BinarySensorEntity): await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) + async def async_will_remove_from_hass(self) -> None: + """Wemo sensor removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + async def async_update(self): """Update WeMo state. diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 1bc477277c..c7325f776f 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -249,6 +249,11 @@ class WemoHumidifier(FanEntity): await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) + async def async_will_remove_from_hass(self) -> None: + """Wemo humidifier removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + async def async_update(self): """Update WeMo state. diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 6aac2be6dd..5d4aa18ee2 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -279,6 +279,11 @@ class WemoDimmer(LightEntity): await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) + async def async_will_remove_from_hass(self) -> None: + """Wemo dimmer removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + async def async_update(self): """Update WeMo state. diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index fc00d4ea8b..e2210d0279 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -220,6 +220,11 @@ class WemoSwitch(SwitchEntity): await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) + async def async_will_remove_from_hass(self) -> None: + """Wemo switch removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + async def async_update(self): """Update WeMo state. From 70133f2096984610cc916e54a1158d9417a10051 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 7 Dec 2020 11:43:35 -0500 Subject: [PATCH 053/302] Fix ZHA switch group test (#44021) --- tests/components/zha/test_switch.py | 55 ++++++++++++++++++----------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 80412d95fb..da3037f720 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -7,6 +7,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f from homeassistant.components.switch import DOMAIN +from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( @@ -67,15 +68,16 @@ async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [general.OnOff.cluster_id], + "in_clusters": [general.OnOff.cluster_id, general.Groups.cluster_id], "out_clusters": [], - "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + "device_type": zha.DeviceType.ON_OFF_SWITCH, } }, ieee=IEEE_GROUPABLE_DEVICE, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -86,15 +88,16 @@ async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [general.OnOff.cluster_id], + "in_clusters": [general.OnOff.cluster_id, general.Groups.cluster_id], "out_clusters": [], - "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + "device_type": zha.DeviceType.ON_OFF_SWITCH, } }, ieee=IEEE_GROUPABLE_DEVICE2, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -157,7 +160,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) -async def async_test_zha_group_switch_entity( +async def test_zha_group_switch_entity( hass, device_switch_1, device_switch_2, coordinator ): """Test the switch entity for a ZHA group.""" @@ -168,30 +171,38 @@ async def async_test_zha_group_switch_entity( device_switch_1._zha_gateway = zha_gateway device_switch_2._zha_gateway = zha_gateway member_ieee_addresses = [device_switch_1.ieee, device_switch_2.ieee] + members = [ + GroupMember(device_switch_1.ieee, 1), + GroupMember(device_switch_2.ieee, 1), + ] # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 2 for member in zha_group.members: - assert member.ieee in member_ieee_addresses + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) assert hass.states.get(entity_id) is not None group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] - dev1_cluster_on_off = device_switch_1.endpoints[1].on_off - dev2_cluster_on_off = device_switch_2.endpoints[1].on_off + dev1_cluster_on_off = device_switch_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off - # test that the lights were created and that they are unavailable + await async_enable_traffic(hass, [device_switch_1, device_switch_2], enabled=False) + await hass.async_block_till_done() + + # test that the lights were created and that they are off assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_group.members) + await async_enable_traffic(hass, [device_switch_1, device_switch_2]) + await hass.async_block_till_done() # test that the lights were created and are off assert hass.states.get(entity_id).state == STATE_OFF @@ -207,7 +218,7 @@ async def async_test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tsn=None + False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None ) assert hass.states.get(entity_id).state == STATE_ON @@ -222,28 +233,32 @@ async def async_test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tsn=None + False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None ) assert hass.states.get(entity_id).state == STATE_OFF # test some of the group logic to make sure we key off states correctly - await dev1_cluster_on_off.on() - await dev2_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is on assert hass.states.get(entity_id).state == STATE_ON - await dev1_cluster_on_off.off() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is still on assert hass.states.get(entity_id).state == STATE_ON - await dev2_cluster_on_off.off() + await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is now off assert hass.states.get(entity_id).state == STATE_OFF - await dev1_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is now back on assert hass.states.get(entity_id).state == STATE_ON From 886ce599ac4724c40bab17bf1f0e98d027e56fc0 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 7 Dec 2020 10:01:58 -0800 Subject: [PATCH 054/302] Bump pymyq to 2.0.11 (#44003) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 0e3d53be08..ee3471725b 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.10"], + "requirements": ["pymyq==2.0.11"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 65092ac608..1e326de802 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.10 +pymyq==2.0.11 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f9775cbd9..3b45bb7938 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.10 +pymyq==2.0.11 # homeassistant.components.nut pynut2==2.1.2 From f18c6ae72ccf9075c3f5442b1aff2381379ca9d6 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Mon, 7 Dec 2020 14:22:23 -0500 Subject: [PATCH 055/302] Add pause and resume services to Rachio (#43944) * Add pause-resume * address comments --- homeassistant/components/rachio/const.py | 3 + homeassistant/components/rachio/device.py | 71 ++++++++++++++++++- homeassistant/components/rachio/services.yaml | 15 ++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 943e1b3319..721fb36fd3 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -49,8 +49,11 @@ KEY_CUSTOM_SLOPE = "customSlope" STATUS_ONLINE = "ONLINE" +MODEL_GENERATION_1 = "GENERATION1" SCHEDULE_TYPE_FIXED = "FIXED" SCHEDULE_TYPE_FLEX = "FLEX" +SERVICE_PAUSE_WATERING = "pause_watering" +SERVICE_RESUME_WATERING = "resume_watering" SERVICE_SET_ZONE_MOISTURE = "set_zone_moisture_percent" SERVICE_START_MULTIPLE_ZONES = "start_multiple_zone_schedule" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 9d7c305793..2ef904f868 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -1,11 +1,14 @@ """Adapter to wrap the rachiopy api for home assistant.""" - import logging from typing import Optional +import voluptuous as vol + from homeassistant.const import EVENT_HOMEASSISTANT_STOP, HTTP_OK +from homeassistant.helpers import config_validation as cv from .const import ( + DOMAIN, KEY_DEVICES, KEY_ENABLED, KEY_EXTERNAL_ID, @@ -19,11 +22,26 @@ from .const import ( KEY_STATUS, KEY_USERNAME, KEY_ZONES, + MODEL_GENERATION_1, + SERVICE_PAUSE_WATERING, + SERVICE_RESUME_WATERING, ) from .webhooks import LISTEN_EVENT_TYPES, WEBHOOK_CONST_ID _LOGGER = logging.getLogger(__name__) +ATTR_DEVICES = "devices" +ATTR_DURATION = "duration" + +PAUSE_SERVICE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DEVICES): cv.string, + vol.Optional(ATTR_DURATION, default=60): cv.positive_int, + } +) + +RESUME_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) + class RachioPerson: """Represent a Rachio user.""" @@ -39,6 +57,8 @@ class RachioPerson: def setup(self, hass): """Rachio device setup.""" + all_devices = [] + can_pause = False response = self.rachio.person.info() assert int(response[0][KEY_STATUS]) == HTTP_OK, "API key error" self._id = response[1][KEY_ID] @@ -68,8 +88,43 @@ class RachioPerson: rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) rachio_iro.setup() self._controllers.append(rachio_iro) + all_devices.append(rachio_iro.name) + # Generation 1 controllers don't support pause or resume + if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: + can_pause = True + _LOGGER.info('Using Rachio API as user "%s"', self.username) + def pause_water(service): + """Service to pause watering on all or specific controllers.""" + duration = service.data[ATTR_DURATION] + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.pause_watering(duration) + + def resume_water(service): + """Service to resume watering on all or specific controllers.""" + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.resume_watering() + + if can_pause: + hass.services.register( + DOMAIN, + SERVICE_PAUSE_WATERING, + pause_water, + schema=PAUSE_SERVICE_SCHEMA, + ) + + hass.services.register( + DOMAIN, + SERVICE_RESUME_WATERING, + resume_water, + schema=RESUME_SERVICE_SCHEMA, + ) + @property def user_id(self) -> str: """Get the user ID as defined by the Rachio API.""" @@ -102,7 +157,7 @@ class RachioIro: self._flex_schedules = data[KEY_FLEX_SCHEDULES] self._init_data = data self._webhooks = webhooks - _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + _LOGGER.debug('%s has ID "%s"', self, self.controller_id) def setup(self): """Rachio Iro setup for webhooks.""" @@ -195,4 +250,14 @@ class RachioIro: def stop_watering(self) -> None: """Stop watering all zones connected to this controller.""" self.rachio.device.stop_water(self.controller_id) - _LOGGER.info("Stopped watering of all zones on %s", str(self)) + _LOGGER.info("Stopped watering of all zones on %s", self) + + def pause_watering(self, duration) -> None: + """Pause watering on this controller.""" + self.rachio.device.pause_zone_run(self.controller_id, duration * 60) + _LOGGER.debug("Paused watering on %s for %s minutes", self, duration) + + def resume_watering(self) -> None: + """Resume paused watering on this controller.""" + self.rachio.device.resume_zone_run(self.controller_id) + _LOGGER.debug("Resuming watering on %s", self) diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 480d53aa45..815a860131 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -16,3 +16,18 @@ start_multiple_zone_schedule: duration: description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zone listed above. [Required] example: 15, 20 +pause_watering: + description: Pause any currently running zones or schedules. + fields: + devices: + description: Name of controllers to pause. Defaults to all controllers on the account if not provided. [Optional] + example: Main House + duration: + description: The number of minutes to pause running schedules. Accepts 1-60. Default is 60 minutes. [Optional] + example: 30 +resume_watering: + description: Resume any paused zone runs or schedules. + fields: + devices: + description: Name of controllers to resume. Defaults to all controllers on the account if not provided. [Optional] + example: Main House From 8632ab9d35c47ee0e7d93e9bc4c54242a1368b00 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Dec 2020 13:34:47 -0700 Subject: [PATCH 056/302] Bump simplisafe-python to 9.6.1 (#44030) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 0f209b366e..eeb37b46df 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.0"], + "requirements": ["simplisafe-python==9.6.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e326de802..e2d87b8b1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2021,7 +2021,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.0 +simplisafe-python==9.6.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b45bb7938..bfb3abbaca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.0 +simplisafe-python==9.6.1 # homeassistant.components.slack slackclient==2.5.0 From 34a31884b7353724db88fea8dfba1391cd86d270 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 8 Dec 2020 00:16:22 +0100 Subject: [PATCH 057/302] Bump dependency to add more multi channel devices to HomematicIP Cloud (#43914) --- .../homematicip_cloud/binary_sensor.py | 45 +- .../components/homematicip_cloud/cover.py | 150 +++++- .../homematicip_cloud/generic_entity.py | 8 +- .../components/homematicip_cloud/light.py | 50 +- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/switch.py | 33 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homematicip_cloud/test_binary_sensor.py | 14 +- .../homematicip_cloud/test_cover.py | 101 +++- .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_light.py | 6 +- .../homematicip_cloud/test_switch.py | 23 +- tests/fixtures/homematicip_cloud.json | 502 ++++++++++++++++++ 14 files changed, 823 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 12bca8378c..57aaa2b0b0 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -6,6 +6,7 @@ from homematicip.aio.device import ( AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, + AsyncFullFlushContactInterface6, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, @@ -91,6 +92,11 @@ async def async_setup_entry( entities.append( HomematicipMultiContactInterface(hap, device, channel=channel) ) + elif isinstance(device, AsyncFullFlushContactInterface6): + for channel in range(1, 7): + entities.append( + HomematicipMultiContactInterface(hap, device, channel=channel) + ) elif isinstance( device, (AsyncContactInterface, AsyncFullFlushContactInterface) ): @@ -224,9 +230,17 @@ class HomematicipTiltVibrationSensor(HomematicipBaseActionSensor): class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP multi room/area contact interface.""" - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: """Initialize the multi contact entity.""" - super().__init__(hap, device, channel=channel) + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def device_class(self) -> str: @@ -244,30 +258,22 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt ) -class HomematicipContactInterface(HomematicipGenericEntity, BinarySensorEntity): +class HomematicipContactInterface(HomematicipMultiContactInterface, BinarySensorEntity): """Representation of the HomematicIP contact interface.""" - @property - def device_class(self) -> str: - """Return the class of this sensor.""" - return DEVICE_CLASS_OPENING - - @property - def is_on(self) -> bool: - """Return true if the contact interface is on/open.""" - if self._device.windowState is None: - return None - return self._device.windowState != WindowState.CLOSED + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the multi contact entity.""" + super().__init__(hap, device, is_multi_channel=False) -class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): +class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEntity): """Representation of the HomematicIP shutter contact.""" def __init__( self, hap: HomematicipHAP, device, has_additional_state: bool = False ) -> None: """Initialize the shutter contact.""" - super().__init__(hap, device) + super().__init__(hap, device, is_multi_channel=False) self.has_additional_state = has_additional_state @property @@ -275,13 +281,6 @@ class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): """Return the class of this sensor.""" return DEVICE_CLASS_DOOR - @property - def is_on(self) -> bool: - """Return true if the shutter contact is on/open.""" - if self._device.windowState is None: - return None - return self._device.windowState != WindowState.CLOSED - @property def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the Shutter Contact.""" diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 3d5af9b7c4..29a06c558f 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -3,6 +3,7 @@ from typing import Optional from homematicip.aio.device import ( AsyncBlindModule, + AsyncDinRailBlind4, AsyncFullFlushBlind, AsyncFullFlushShutter, AsyncGarageDoorModuleTormatic, @@ -37,6 +38,11 @@ async def async_setup_entry( for device in hap.home.devices: if isinstance(device, AsyncBlindModule): entities.append(HomematicipBlindModule(hap, device)) + elif isinstance(device, AsyncDinRailBlind4): + for channel in range(1, 5): + entities.append( + HomematicipMultiCoverSlats(hap, device, channel=channel) + ) elif isinstance(device, AsyncFullFlushBlind): entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): @@ -130,14 +136,28 @@ class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): await self._device.stop() -class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): +class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): """Representation of the HomematicIP cover shutter.""" + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: + """Initialize the multi cover entity.""" + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) + @property def current_cover_position(self) -> int: """Return current position of cover.""" - if self._device.shutterLevel is not None: - return int((1 - self._device.shutterLevel) * 100) + if self._device.functionalChannels[self._channel].shutterLevel is not None: + return int( + (1 - self._device.functionalChannels[self._channel].shutterLevel) * 100 + ) return None async def async_set_cover_position(self, **kwargs) -> None: @@ -145,36 +165,61 @@ class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 level = 1 - position / 100.0 - await self._device.set_shutter_level(level) + await self._device.set_shutter_level(level, self._channel) @property def is_closed(self) -> Optional[bool]: """Return if the cover is closed.""" - if self._device.shutterLevel is not None: - return self._device.shutterLevel == HMIP_COVER_CLOSED + if self._device.functionalChannels[self._channel].shutterLevel is not None: + return ( + self._device.functionalChannels[self._channel].shutterLevel + == HMIP_COVER_CLOSED + ) return None async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" - await self._device.set_shutter_level(HMIP_COVER_OPEN) + await self._device.set_shutter_level(HMIP_COVER_OPEN, self._channel) async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" - await self._device.set_shutter_level(HMIP_COVER_CLOSED) + await self._device.set_shutter_level(HMIP_COVER_CLOSED, self._channel) async def async_stop_cover(self, **kwargs) -> None: """Stop the device if in motion.""" - await self._device.set_shutter_stop() + await self._device.set_shutter_stop(self._channel) -class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): - """Representation of the HomematicIP cover slats.""" +class HomematicipCoverShutter(HomematicipMultiCoverShutter, CoverEntity): + """Representation of the HomematicIP cover shutter.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the multi cover entity.""" + super().__init__(hap, device, is_multi_channel=False) + + +class HomematicipMultiCoverSlats(HomematicipMultiCoverShutter, CoverEntity): + """Representation of the HomematicIP multi cover slats.""" + + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: + """Initialize the multi slats entity.""" + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" - if self._device.slatsLevel is not None: - return int((1 - self._device.slatsLevel) * 100) + if self._device.functionalChannels[self._channel].slatsLevel is not None: + return int( + (1 - self._device.functionalChannels[self._channel].slatsLevel) * 100 + ) return None async def async_set_cover_tilt_position(self, **kwargs) -> None: @@ -182,19 +227,27 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 - await self._device.set_slats_level(level) + await self._device.set_slats_level(level, self._channel) async def async_open_cover_tilt(self, **kwargs) -> None: """Open the slats.""" - await self._device.set_slats_level(HMIP_SLATS_OPEN) + await self._device.set_slats_level(HMIP_SLATS_OPEN, self._channel) async def async_close_cover_tilt(self, **kwargs) -> None: """Close the slats.""" - await self._device.set_slats_level(HMIP_SLATS_CLOSED) + await self._device.set_slats_level(HMIP_SLATS_CLOSED, self._channel) async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" - await self._device.set_shutter_stop() + await self._device.set_shutter_stop(self._channel) + + +class HomematicipCoverSlats(HomematicipMultiCoverSlats, CoverEntity): + """Representation of the HomematicIP cover slats.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the multi slats entity.""" + super().__init__(hap, device, is_multi_channel=False) class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): @@ -229,10 +282,69 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): await self._device.send_door_command(DoorCommand.STOP) -class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverEntity): +class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): """Representation of the HomematicIP cover shutter group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None: """Initialize switching group.""" device.modelType = f"HmIP-{post}" - super().__init__(hap, device, post) + super().__init__(hap, device, post, is_multi_channel=False) + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + if self._device.shutterLevel is not None: + return int((1 - self._device.shutterLevel) * 100) + return None + + @property + def current_cover_tilt_position(self) -> int: + """Return current tilt position of cover.""" + if self._device.slatsLevel is not None: + return int((1 - self._device.slatsLevel) * 100) + return None + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + if self._device.shutterLevel is not None: + return self._device.shutterLevel == HMIP_COVER_CLOSED + return None + + async def async_set_cover_position(self, **kwargs) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + # HmIP cover is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_shutter_level(level) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Move the cover to a specific tilt position.""" + position = kwargs[ATTR_TILT_POSITION] + # HmIP slats is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_slats_level(level) + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.set_shutter_level(HMIP_COVER_OPEN) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.set_shutter_level(HMIP_COVER_CLOSED) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the group if in motion.""" + await self._device.set_shutter_stop() + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open the slats.""" + await self._device.set_slats_level(HMIP_SLATS_OPEN) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close the slats.""" + await self._device.set_slats_level(HMIP_SLATS_CLOSED) + + async def async_stop_cover_tilt(self, **kwargs) -> None: + """Stop the group if in motion.""" + await self._device.set_shutter_stop() diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index ce8b44f570..a8df0107ee 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -76,6 +76,7 @@ class HomematicipGenericEntity(Entity): device, post: Optional[str] = None, channel: Optional[int] = None, + is_multi_channel: Optional[bool] = False, ) -> None: """Initialize the generic entity.""" self._hap = hap @@ -83,6 +84,7 @@ class HomematicipGenericEntity(Entity): self._device = device self._post = post self._channel = channel + self._is_multi_channel = is_multi_channel # Marker showing that the HmIP device hase been removed. self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @@ -179,7 +181,7 @@ class HomematicipGenericEntity(Entity): name = None # Try to get a label from a channel. if hasattr(self._device, "functionalChannels"): - if self._channel: + if self._is_multi_channel: name = self._device.functionalChannels[self._channel].label else: if len(self._device.functionalChannels) > 1: @@ -190,7 +192,7 @@ class HomematicipGenericEntity(Entity): name = self._device.label if self._post: name = f"{name} {self._post}" - elif self._channel: + elif self._is_multi_channel: name = f"{name} Channel{self._channel}" # Add a prefix to the name if the homematic ip home has a name. @@ -213,7 +215,7 @@ class HomematicipGenericEntity(Entity): def unique_id(self) -> str: """Return a unique ID.""" unique_id = f"{self.__class__.__name__}_{self._device.id}" - if self._channel: + if self._is_multi_channel: unique_id = ( f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}" ) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index f0c191ac1a..1909ff818b 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -106,9 +106,17 @@ class HomematicipLightMeasuring(HomematicipLight): class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): """Representation of HomematicIP Cloud dimmer.""" - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: """Initialize the dimmer light entity.""" - super().__init__(hap, device, channel=channel) + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def is_on(self) -> bool: @@ -142,38 +150,12 @@ class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): await self._device.set_dim_level(0, self._channel) -class HomematicipDimmer(HomematicipGenericEntity, LightEntity): +class HomematicipDimmer(HomematicipMultiDimmer, LightEntity): """Representation of HomematicIP Cloud dimmer.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the dimmer light entity.""" - super().__init__(hap, device) - - @property - def is_on(self) -> bool: - """Return true if dimmer is on.""" - return self._device.dimLevel is not None and self._device.dimLevel > 0.0 - - @property - def brightness(self) -> int: - """Return the brightness of this light between 0..255.""" - return int((self._device.dimLevel or 0.0) * 255) - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - async def async_turn_on(self, **kwargs) -> None: - """Turn the dimmer on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) - else: - await self._device.set_dim_level(1) - - async def async_turn_off(self, **kwargs) -> None: - """Turn the dimmer off.""" - await self._device.set_dim_level(0) + super().__init__(hap, device, is_multi_channel=False) class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): @@ -182,9 +164,13 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the notification light entity.""" if channel == 2: - super().__init__(hap, device, post="Top", channel=channel) + super().__init__( + hap, device, post="Top", channel=channel, is_multi_channel=True + ) else: - super().__init__(hap, device, post="Bottom", channel=channel) + super().__init__( + hap, device, post="Bottom", channel=channel, is_multi_channel=True + ) self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 30ca5165c8..9f04569446 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.12.1"], + "requirements": ["homematicip==0.13.0"], "codeowners": ["@SukramJ"], "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 72f9f94c21..9047ed9095 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -3,6 +3,7 @@ from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, + AsyncDinRailSwitch4, AsyncFullFlushInputSwitch, AsyncFullFlushSwitchMeasuring, AsyncHeatingSwitch2, @@ -44,6 +45,9 @@ async def async_setup_entry( elif isinstance(device, AsyncWiredSwitch8): for channel in range(1, 9): entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) + elif isinstance(device, AsyncDinRailSwitch4): + for channel in range(1, 5): + entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) elif isinstance( device, ( @@ -77,9 +81,17 @@ async def async_setup_entry( class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): """Representation of the HomematicIP multi switch.""" - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: """Initialize the multi switch device.""" - super().__init__(hap, device, channel=channel) + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def is_on(self) -> bool: @@ -95,25 +107,12 @@ class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): await self._device.turn_off(self._channel) -class HomematicipSwitch(HomematicipGenericEntity, SwitchEntity): +class HomematicipSwitch(HomematicipMultiSwitch, SwitchEntity): """Representation of the HomematicIP switch.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the switch device.""" - super().__init__(hap, device) - - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return self._device.on - - async def async_turn_on(self, **kwargs) -> None: - """Turn the device on.""" - await self._device.turn_on() - - async def async_turn_off(self, **kwargs) -> None: - """Turn the device off.""" - await self._device.turn_off() + super().__init__(hap, device, is_multi_channel=False) class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): diff --git a/requirements_all.txt b/requirements_all.txt index e2d87b8b1a..4eddfe6dc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -774,7 +774,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.12.1 +homematicip==0.13.0 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfb3abbaca..92c4096736 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -403,7 +403,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.12.1 +homematicip==0.13.0 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 420977cd40..c392266666 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -540,13 +540,13 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap_factory): assert ha_state.state == STATE_ON -async def test_hmip_wired_multi_contact_interface(hass, default_mock_hap_factory): +async def test_hmip_multi_contact_interface(hass, default_mock_hap_factory): """Test HomematicipMultiContactInterface.""" entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel5" entity_name = "Wired Eingangsmodul – 32-fach Channel5" device_model = "HmIPW-DRI32" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=["Wired Eingangsmodul – 32-fach"] + test_devices=["Wired Eingangsmodul – 32-fach", "Licht Flur"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -563,3 +563,13 @@ async def test_hmip_wired_multi_contact_interface(hass, default_mock_hap_factory await async_manipulate_test_data(hass, hmip_device, "windowState", None, channel=5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + + ha_state, hmip_device = get_and_check_entity_basics( + hass, + mock_hap, + "binary_sensor.licht_flur_5", + "Licht Flur 5", + "HmIP-FCI6", + ) + + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 82d2f41de5..a35576ed35 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -43,7 +43,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "set_shutter_level" - assert hmip_device.mock_calls[-1][1] == (0,) + assert hmip_device.mock_calls[-1][1] == (0, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -57,7 +57,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "set_shutter_level" - assert hmip_device.mock_calls[-1][1] == (0.5,) + assert hmip_device.mock_calls[-1][1] == (0.5, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0.5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -68,7 +68,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 5 assert hmip_device.mock_calls[-1][0] == "set_shutter_level" - assert hmip_device.mock_calls[-1][1] == (1,) + assert hmip_device.mock_calls[-1][1] == (1, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_CLOSED @@ -79,7 +79,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 7 assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) @@ -109,7 +109,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0,) + assert hmip_device.mock_calls[-1][1] == (0, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0) ha_state = hass.states.get(entity_id) @@ -125,7 +125,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 4 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0.5,) + assert hmip_device.mock_calls[-1][1] == (0.5, 1) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -137,7 +137,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 6 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (1,) + assert hmip_device.mock_calls[-1][1] == (1, 1) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -149,7 +149,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 8 assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", None) ha_state = hass.states.get(entity_id) @@ -160,6 +160,84 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): assert ha_state.state == STATE_UNKNOWN +async def test_hmip_multi_cover_slats(hass, default_mock_hap_factory): + """Test HomematicipCoverSlats.""" + entity_id = "cover.wohnzimmer_fenster" + entity_name = "Wohnzimmer Fenster" + device_model = "HmIP-DRBLI4" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Jalousieaktor 1 für Hutschienenmontage – 4-fach"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1, channel=4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1, channel=4) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0, 4) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0, channel=4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0.5, 4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (1, 4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == (4,) + + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", None, channel=4) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN + + async def test_hmip_blind_module(hass, default_mock_hap_factory): """Test HomematicipBlindModule.""" entity_id = "cover.sonnenschutz_balkontur" @@ -254,6 +332,13 @@ async def test_hmip_blind_module(hass, default_mock_hap_factory): assert hmip_device.mock_calls[-1][0] == "stop" assert hmip_device.mock_calls[-1][1] == () + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 14 + assert hmip_device.mock_calls[-1][0] == "stop" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", None) ha_state = hass.states.get(entity_id) assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION) diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 4047a8ef28..0e69a67cdb 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 236 + assert len(mock_hap.hmip_device_by_entity_id) == 250 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index b62a98fd03..b4dbd0d140 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -175,7 +175,7 @@ async def test_hmip_dimmer(hass, default_mock_hap_factory): "light", "turn_on", {"entity_id": entity_id}, blocking=True ) assert hmip_device.mock_calls[-1][0] == "set_dim_level" - assert hmip_device.mock_calls[-1][1] == (1,) + assert hmip_device.mock_calls[-1][1] == (1, 1) await hass.services.async_call( "light", @@ -185,7 +185,7 @@ async def test_hmip_dimmer(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 2 assert hmip_device.mock_calls[-1][0] == "set_dim_level" - assert hmip_device.mock_calls[-1][1] == (1.0,) + assert hmip_device.mock_calls[-1][1] == (1.0, 1) await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -196,7 +196,7 @@ async def test_hmip_dimmer(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 4 assert hmip_device.mock_calls[-1][0] == "set_dim_level" - assert hmip_device.mock_calls[-1][1] == (0,) + assert hmip_device.mock_calls[-1][1] == (0, 1) await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 034ca33aec..f2b3dfba32 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -43,7 +43,7 @@ async def test_hmip_switch(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "turn_off" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -53,7 +53,7 @@ async def test_hmip_switch(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "turn_on" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", True) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -80,7 +80,7 @@ async def test_hmip_switch_input(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "turn_off" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -90,7 +90,7 @@ async def test_hmip_switch_input(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "turn_on" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", True) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -117,7 +117,7 @@ async def test_hmip_switch_measuring(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "turn_off" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -127,7 +127,7 @@ async def test_hmip_switch_measuring(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "turn_on" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", True) await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) @@ -191,6 +191,7 @@ async def test_hmip_multi_switch(hass, default_mock_hap_factory): "Multi IO Box", "Heizungsaktor", "ioBroker", + "Schaltaktor Verteiler", ] ) @@ -221,6 +222,16 @@ async def test_hmip_multi_switch(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + ha_state, hmip_device = get_and_check_entity_basics( + hass, + mock_hap, + "switch.schaltaktor_verteiler_channel3", + "Schaltaktor Verteiler Channel3", + "HmIP-DRSI4", + ) + + assert ha_state.state == STATE_OFF + async def test_hmip_wired_multi_switch(hass, default_mock_hap_factory): """Test HomematicipMultiSwitch.""" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 7adf4b85b1..beb7e42400 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -6116,6 +6116,508 @@ "serializedGlobalTradeItemNumber": "3014F0000000000000FAF9B4", "type": "TORMATIC_MODULE", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000005521": { + "availableFirmwareVersion": "1.4.2", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.4.2", + "firmwareVersionInteger": 66562, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000005521", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -82, + "rssiPeerValue": -78, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": true, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "index": 1, + "label": "Poolpumpe", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "index": 2, + "label": "Poollicht", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000005521", + "label": "Schaltaktor Verteiler", + "lastStatusUpdate": 1605271783993, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 405, + "modelType": "HmIP-DRSI4", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000005521", + "type": "DIN_RAIL_SWITCH_4", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000022311": { + "availableFirmwareVersion": "1.6.0", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.6.0", + "firmwareVersionInteger": 67072, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000022311", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000026" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -70, + "rssiPeerValue": -63, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": true, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 18.19999999999999, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000027" + ], + "index": 1, + "label": "Badezimmer ", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 17.49999999999998, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 17.899999999999984, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000028" + ], + "index": 2, + "label": "Schlafzimmer ", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 17.399999999999977, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 27.300000000000118, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 3, + "label": "Wohnzimmer T\u00fcr", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 24.400000000000077, + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 25.900000000000098, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 4, + "label": "Wohnzimmer Fenster", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 25.000000000000085, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000022311", + "label": "Jalousieaktor 1 f\u00fcr Hutschienenmontage \u2013 4-fach", + "lastStatusUpdate": 1604414124509, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 406, + "modelType": "HmIP-DRBLI4", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000022311", + "type": "DIN_RAIL_BLIND_4", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000056775": { + "availableFirmwareVersion": "1.0.16", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.16", + "firmwareVersionInteger": 65552, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000056775", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000043" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true + }, + "temperatureOutOfRange": false, + "unreach": null + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000044", + "00000000-0000-0000-0000-000000000045" + ], + "index": 1, + "label": "Licht Flur 1", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000044", + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000047" + ], + "index": 2, + "label": "Licht Flur 2", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 3, + "label": "Tür", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": true + }, + "windowState": "OPEN" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 4, + "label": "Licht Flur 4", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "5": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 5, + "label": "Licht Flur 5", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "6": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 6, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 6, + "label": "Licht Flur 6", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000056775", + "label": "Licht Flur", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 379, + "modelType": "HmIP-FCI6", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000056775", + "type": "FULL_FLUSH_CONTACT_INTERFACE_6", + "updateState": "UP_TO_DATE" } }, "groups": { From ebe57d4fdb143aba0f9d0281de76b129e0597cf2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 8 Dec 2020 00:04:13 +0000 Subject: [PATCH 058/302] [ci skip] Translation update --- .../components/gios/translations/en.json | 5 +++++ .../components/gios/translations/et.json | 5 +++++ .../components/gios/translations/no.json | 5 +++++ .../components/gios/translations/pl.json | 5 +++++ .../components/gios/translations/ru.json | 5 +++++ .../components/gios/translations/zh-Hans.json | 7 +++++++ .../components/gios/translations/zh-Hant.json | 5 +++++ .../components/hassio/translations/nl.json | 15 +++++++++++++++ .../components/homeassistant/translations/nl.json | 15 +++++++++++++++ .../components/kulersky/translations/ca.json | 8 ++++++++ .../components/mobile_app/translations/ca.json | 5 +++++ .../components/twinkly/translations/nl.json | 13 +++++++++++++ 12 files changed, 93 insertions(+) create mode 100644 homeassistant/components/gios/translations/zh-Hans.json create mode 100644 homeassistant/components/homeassistant/translations/nl.json create mode 100644 homeassistant/components/kulersky/translations/ca.json create mode 100644 homeassistant/components/twinkly/translations/nl.json diff --git a/homeassistant/components/gios/translations/en.json b/homeassistant/components/gios/translations/en.json index abc49b1f5a..86f05b8987 100644 --- a/homeassistant/components/gios/translations/en.json +++ b/homeassistant/components/gios/translations/en.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach GIO\u015a server" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/et.json b/homeassistant/components/gios/translations/et.json index 163407ffce..2d0906f73a 100644 --- a/homeassistant/components/gios/translations/et.json +++ b/homeassistant/components/gios/translations/et.json @@ -18,5 +18,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendus GIO\u015a serveriga" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json index d80e3bcae1..038cbdc20a 100644 --- a/homeassistant/components/gios/translations/no.json +++ b/homeassistant/components/gios/translations/no.json @@ -18,5 +18,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 GIO\u015a-server" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/pl.json b/homeassistant/components/gios/translations/pl.json index 1b35ab9899..8bc909e2ba 100644 --- a/homeassistant/components/gios/translations/pl.json +++ b/homeassistant/components/gios/translations/pl.json @@ -18,5 +18,10 @@ "title": "G\u0142\u00f3wny Inspektorat Ochrony \u015arodowiska (GIO\u015a)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ru.json b/homeassistant/components/gios/translations/ru.json index 826cfc22d4..68d6ee44b0 100644 --- a/homeassistant/components/gios/translations/ru.json +++ b/homeassistant/components/gios/translations/ru.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u043a\u0430\u044f \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b)" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/zh-Hans.json b/homeassistant/components/gios/translations/zh-Hans.json new file mode 100644 index 0000000000..72430b5e15 --- /dev/null +++ b/homeassistant/components/gios/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee GIO\u015a \u670d\u52a1\u5668" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/zh-Hant.json b/homeassistant/components/gios/translations/zh-Hant.json index 4b8668e08a..d72bc9bc01 100644 --- a/homeassistant/components/gios/translations/zh-Hant.json +++ b/homeassistant/components/gios/translations/zh-Hant.json @@ -18,5 +18,10 @@ "title": "GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda GIO\u015a \u4f3a\u670d\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index 981cb51c83..fca08d49d7 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "disk_total": "Totale schijfruimte", + "disk_used": "Gebruikte schijfruimte", + "docker_version": "Docker versie", + "healthy": "Gezond", + "host_os": "Host-besturingssysteem", + "installed_addons": "Ge\u00efnstalleerde add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor versie", + "supported": "Ondersteund", + "update_channel": "Update kanaal", + "version_api": "API Versie" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json new file mode 100644 index 0000000000..a4f73279a7 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -0,0 +1,15 @@ +{ + "system_health": { + "info": { + "dev": "Ontwikkelaarsmodus", + "docker": "Docker", + "docker_version": "Docker", + "os_version": "Versie van het besturingssysteem", + "python_version": "Python versie", + "supervisor": "Supervisor", + "timezone": "Tijdzone", + "version": "Versie", + "virtualenv": "Virtuele omgeving" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ca.json b/homeassistant/components/kulersky/translations/ca.json new file mode 100644 index 0000000000..7d765f80f8 --- /dev/null +++ b/homeassistant/components/kulersky/translations/ca.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'ha trobat cap dispositiu a la xarxa", + "single_instance_allowed": "Ja est\u00e0 configurat. Nom\u00e9s una configuraci\u00f3 \u00e9s possible" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index bb070f391a..4e857279e9 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -8,5 +8,10 @@ "description": "Vols configurar el component d'aplicaci\u00f3 m\u00f2bil?" } } + }, + "device_automation": { + "action_type": { + "notify": "Enviar una notificaci\u00f3" + } } } \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json new file mode 100644 index 0000000000..861ee57283 --- /dev/null +++ b/homeassistant/components/twinkly/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostnaam (of IP-adres van uw Twinkly apparaat" + }, + "description": "Uw Twinkly LED-string instellen", + "title": "Twinkly" + } + } + } +} \ No newline at end of file From e0bcee1cc3b1de556247db2f3337780373573e36 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 7 Dec 2020 20:06:32 -0500 Subject: [PATCH 059/302] Update ZHA dependencies (#44039) zha-quirks==0.0.48 zigpy==0.28.2 zigpy-znp==0.3.0 --- homeassistant/components/zha/manifest.json | 7 ++++--- requirements_all.txt | 7 ++++--- requirements_test_all.txt | 10 +++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 780bb5bc99..f1821c9e48 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,13 +6,14 @@ "requirements": [ "bellows==0.21.0", "pyserial==3.4", - "zha-quirks==0.0.47", + "pyserial-asyncio==0.4", + "zha-quirks==0.0.48", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.28.1", + "zigpy==0.28.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.2.2" + "zigpy-znp==0.3.0" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4eddfe6dc7..e6b68edcac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1661,6 +1661,7 @@ pysdcp==1 pysensibo==1.0.3 # homeassistant.components.serial +# homeassistant.components.zha pyserial-asyncio==0.4 # homeassistant.components.acer_projector @@ -2344,7 +2345,7 @@ zengge==0.2 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.47 +zha-quirks==0.0.48 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2365,10 +2366,10 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.2.2 +zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.1 +zigpy==0.28.2 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92c4096736..781832d82b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -834,6 +834,10 @@ pyrisco==0.3.1 # homeassistant.components.ruckus_unleashed pyruckus==0.12 +# homeassistant.components.serial +# homeassistant.components.zha +pyserial-asyncio==0.4 + # homeassistant.components.acer_projector # homeassistant.components.zha pyserial==3.4 @@ -1140,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.47 +zha-quirks==0.0.48 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1155,7 +1159,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.2.2 +zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.1 +zigpy==0.28.2 From 5f4f6dea6bab1d47359b204e2440601d1617a5c7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Dec 2020 19:57:19 -0700 Subject: [PATCH 060/302] Bump simplisafe-python to 9.6.2 (#44040) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index eeb37b46df..a502a7908f 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.1"], + "requirements": ["simplisafe-python==9.6.2"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6b68edcac..bd7e347430 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2022,7 +2022,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.1 +simplisafe-python==9.6.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 781832d82b..b15a2d7afc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.1 +simplisafe-python==9.6.2 # homeassistant.components.slack slackclient==2.5.0 From 572d4cfbe65391b4438d4ea4cfe4e1fbf604b7c8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Dec 2020 08:19:57 +0100 Subject: [PATCH 061/302] Add the missing ATTR_ENABLED attribute to Brother integration list of sensors (#44036) --- homeassistant/components/brother/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index cbb3d2a70c..5aecde1632 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -136,6 +136,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", From fca8841e34808d698d2667072a076ca1f58f8811 Mon Sep 17 00:00:00 2001 From: Alex Szlavik Date: Tue, 8 Dec 2020 05:32:48 -0500 Subject: [PATCH 062/302] Retry tuya setup on auth rate limiting (#44001) Co-authored-by: Martin Hjelmare --- homeassistant/components/tuya/__init__.py | 5 +++++ homeassistant/components/tuya/config_flow.py | 14 ++++++++++++-- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/tuya/strings.json | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index bc665baeb8..5876331ea9 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -6,6 +6,7 @@ import logging from tuyaha import TuyaApi from tuyaha.tuyaapi import ( TuyaAPIException, + TuyaAPIRateLimitException, TuyaFrequentlyInvokeException, TuyaNetException, TuyaServerException, @@ -137,6 +138,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) as exc: raise ConfigEntryNotReady() from exc + except TuyaAPIRateLimitException as exc: + _LOGGER.error("Tuya login rate limited") + raise ConfigEntryNotReady() from exc + except TuyaAPIException as exc: _LOGGER.error( "Connection error during integration setup. Error: %s", diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index e2048aaf7b..5d22a83e03 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -2,7 +2,12 @@ import logging from tuyaha import TuyaApi -from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException +from tuyaha.tuyaapi import ( + TuyaAPIException, + TuyaAPIRateLimitException, + TuyaNetException, + TuyaServerException, +) import voluptuous as vol from homeassistant import config_entries @@ -103,7 +108,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): tuya.init( self._username, self._password, self._country_code, self._platform ) - except (TuyaNetException, TuyaServerException): + except (TuyaAPIRateLimitException, TuyaNetException, TuyaServerException): return RESULT_CONN_ERROR except TuyaAPIException: return RESULT_AUTH_FAILED @@ -249,6 +254,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + + if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + _LOGGER.error("Tuya integration not yet loaded") + return self.async_abort(reason="cannot_connect") + if user_input is not None: dev_ids = user_input.get(CONF_LIST_DEVICES) if dev_ids: diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 642a4dbe5d..7481e56f00 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuyaha==0.0.8"], + "requirements": ["tuyaha==0.0.9"], "codeowners": ["@ollo69"], "config_flow": true } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 8457590601..444ff0b5c2 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "step": { "init": { "title": "Configure Tuya Options", diff --git a/requirements_all.txt b/requirements_all.txt index bd7e347430..e9daee8f38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2208,7 +2208,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.8 +tuyaha==0.0.9 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b15a2d7afc..dc4210dd8f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1073,7 +1073,7 @@ total_connect_client==0.55 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.8 +tuyaha==0.0.9 # homeassistant.components.twentemilieu twentemilieu==0.3.0 From 0b7b6b1d81e3fe1dca93af2db43581d10b2dfc22 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 8 Dec 2020 12:00:17 +0000 Subject: [PATCH 063/302] =?UTF-8?q?Bump=20ciscomobilityexpress=20version:?= =?UTF-8?q?=200.3.3=20=E2=86=92=200.3.9=20(#44050)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update manifest.json * Update requirements_all.txt --- homeassistant/components/cisco_mobility_express/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index 972903e53e..b34daaa6d1 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -2,6 +2,6 @@ "domain": "cisco_mobility_express", "name": "Cisco Mobility Express", "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", - "requirements": ["ciscomobilityexpress==0.3.3"], + "requirements": ["ciscomobilityexpress==0.3.9"], "codeowners": ["@fbradyirl"] } diff --git a/requirements_all.txt b/requirements_all.txt index e9daee8f38..595cebfe29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ caldav==0.6.1 circuit-webhook==1.0.1 # homeassistant.components.cisco_mobility_express -ciscomobilityexpress==0.3.3 +ciscomobilityexpress==0.3.9 # homeassistant.components.cppm_tracker clearpasspy==1.0.2 From ac2af69d26eae622fcaee6330aa9593579bb90c9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Dec 2020 13:06:29 +0100 Subject: [PATCH 064/302] Fix extracting entity and device IDs from scripts (#44048) * Fix extracting entity and device IDs from scripts * Fix extracting from data_template --- homeassistant/helpers/script.py | 66 ++++++++++++------- tests/components/automation/test_init.py | 3 + .../blueprint/test_websocket_api.py | 2 +- tests/helpers/test_script.py | 41 +++++++++++- .../automation/test_event_service.yaml | 1 + 5 files changed, 86 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 645131b60b..48a662e3a8 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -22,10 +22,10 @@ from async_timeout import timeout import voluptuous as vol from homeassistant import exceptions -import homeassistant.components.device_automation as device_automation +from homeassistant.components import device_automation, scene from homeassistant.components.logger import LOGSEVERITY -import homeassistant.components.scene as scene from homeassistant.const import ( + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_ALIAS, CONF_CHOOSE, @@ -44,6 +44,7 @@ from homeassistant.const import ( CONF_REPEAT, CONF_SCENE, CONF_SEQUENCE, + CONF_TARGET, CONF_TIMEOUT, CONF_UNTIL, CONF_VARIABLES, @@ -60,13 +61,9 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.helpers import condition, config_validation as cv, template +from homeassistant.helpers import condition, config_validation as cv, service, template from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables -from homeassistant.helpers.service import ( - CONF_SERVICE_DATA, - async_prepare_call_from_config, -) from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -429,13 +426,13 @@ class _ScriptRun: self._script.last_action = self._action.get(CONF_ALIAS, "call service") self._log("Executing step %s", self._script.last_action) - domain, service, service_data = async_prepare_call_from_config( + domain, service_name, service_data = service.async_prepare_call_from_config( self._hass, self._action, self._variables ) running_script = ( domain == "automation" - and service == "trigger" + and service_name == "trigger" or domain in ("python_script", "script") ) # If this might start a script then disable the call timeout. @@ -448,7 +445,7 @@ class _ScriptRun: service_task = self._hass.async_create_task( self._hass.services.async_call( domain, - service, + service_name, service_data, blocking=True, context=self._context, @@ -755,6 +752,23 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[Dict[str, Any], MappingProxyType] +def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None: + """Extract referenced IDs.""" + if not data: + return + + item_ids = data.get(key) + + if item_ids is None or isinstance(item_ids, template.Template): + return + + if isinstance(item_ids, str): + item_ids = [item_ids] + + for item_id in item_ids: + found.add(item_id) + + class Script: """Representation of a script.""" @@ -889,7 +903,16 @@ class Script: for step in self.sequence: action = cv.determine_script_action(step) - if action == cv.SCRIPT_ACTION_CHECK_CONDITION: + if action == cv.SCRIPT_ACTION_CALL_SERVICE: + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) + + elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_devices(step) elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: @@ -910,20 +933,13 @@ class Script: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: - data = step.get(CONF_SERVICE_DATA) - if not data: - continue - - entity_ids = data.get(ATTR_ENTITY_ID) - - if entity_ids is None or isinstance(entity_ids, template.Template): - continue - - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - for entity_id in entity_ids: - referenced.add(entity_id) + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_entities(step) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 95667d9a69..5f258fc28b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1254,3 +1254,6 @@ async def test_blueprint_automation(hass, calls): hass.bus.async_fire("blueprint_event") await hass.async_block_till_done() assert len(calls) == 1 + assert automation.entities_in_automation(hass, "automation.automation_0") == [ + "light.kitchen" + ] diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index c4f39127d9..bb08414b6e 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8f9e3cec36..92666335f2 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1338,6 +1338,18 @@ async def test_referenced_entities(hass): "service": "test.script", "data": {"entity_id": "{{ 'light.service_template' }}"}, }, + { + "service": "test.script", + "entity_id": "light.direct_entity_referenced", + }, + { + "service": "test.script", + "target": {"entity_id": "light.entity_in_target"}, + }, + { + "service": "test.script", + "data_template": {"entity_id": "light.entity_in_data_template"}, + }, { "condition": "state", "entity_id": "sensor.condition", @@ -1357,6 +1369,9 @@ async def test_referenced_entities(hass): "light.service_list", "sensor.condition", "scene.hello", + "light.direct_entity_referenced", + "light.entity_in_target", + "light.entity_in_data_template", } # Test we cache results. assert script_obj.referenced_entities is script_obj.referenced_entities @@ -1374,12 +1389,36 @@ async def test_referenced_devices(hass): "device_id": "condition-dev-id", "domain": "switch", }, + { + "service": "test.script", + "data": {"device_id": "data-string-id"}, + }, + { + "service": "test.script", + "data_template": {"device_id": "data-template-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": "target-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": ["target-list-id-1", "target-list-id-2"]}, + }, ] ), "Test Name", "test_domain", ) - assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"} + assert script_obj.referenced_devices == { + "script-dev-id", + "condition-dev-id", + "data-string-id", + "data-template-string-id", + "target-string-id", + "target-list-id-1", + "target-list-id-2", + } # Test we cache results. assert script_obj.referenced_devices is script_obj.referenced_devices diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index eff8b52db1..ab067b004a 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -9,3 +9,4 @@ trigger: event_type: !input trigger_event action: service: !input service_to_call + entity_id: light.kitchen From faf21e1e1a40f66dc024609456333c533ce01bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 8 Dec 2020 14:16:31 +0100 Subject: [PATCH 065/302] Bump pyatv to 0.7.5 (#44051) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index ccd5da4954..21b2df308d 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ - "pyatv==0.7.3" + "pyatv==0.7.5" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 595cebfe29..849caa4a81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ pyatmo==4.2.1 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.3 +pyatv==0.7.5 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc4210dd8f..0f0c365f03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -649,7 +649,7 @@ pyatag==0.3.4.4 pyatmo==4.2.1 # homeassistant.components.apple_tv -pyatv==0.7.3 +pyatv==0.7.5 # homeassistant.components.blackbird pyblackbird==0.5 From 109ce653c002d80ce85b0748a75f0ce6b6356ef4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 8 Dec 2020 17:01:07 +0000 Subject: [PATCH 066/302] Fix how homekit_controller enumerates Hue remote (#44019) --- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_hue_bridge.py | 19 +++++++++---------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 45c493ad86..9580a7ee50 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.57" + "aiohomekit==0.2.60" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 849caa4a81..62baa006c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f0c365f03..02ecc56b6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 67b7508eb9..168ae85b22 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -51,16 +51,15 @@ async def test_hue_bridge_setup(hass): ] for button in ("button1", "button2", "button3", "button4"): - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": button, - "subtype": subtype, - } - ) + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": "single_press", + } + ) triggers = await async_get_device_automations(hass, "trigger", device.id) assert_lists_same(triggers, expected) From 51f9da94e12a34906421ec64fb023db69267f418 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 8 Dec 2020 14:34:26 -0500 Subject: [PATCH 067/302] Exclude coordinator when looking up group members entity IDs (#44058) --- homeassistant/components/zha/core/group.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 8edb1da8f6..59277a394b 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -194,6 +194,8 @@ class ZHAGroup(LogMixin): """Return entity ids from the entity domain for this group.""" domain_entity_ids: List[str] = [] for member in self.members: + if member.device.is_coordinator: + continue entities = async_entries_for_device( self._zha_gateway.ha_entity_registry, member.device.device_id, From 2fdf32bf1b48901a637c4a880bca5cad2677d953 Mon Sep 17 00:00:00 2001 From: Fuzzy <16689090+FuzzyMistborn@users.noreply.github.com> Date: Tue, 8 Dec 2020 15:10:50 -0500 Subject: [PATCH 068/302] Add T8400 to ignore list (#44017) --- homeassistant/components/homekit_controller/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 71c8005cbc..e046a131a6 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -22,8 +22,9 @@ HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" HOMEKIT_IGNORE = [ - # eufy Indoor Cam 2K Pan & Tilt + # eufy Indoor Cam 2K and 2K Pan & Tilt # https://github.com/home-assistant/core/issues/42307 + "T8400", "T8410", # Hive Hub - vendor does not give user a pairing code "HHKBridge1,1", From 07461f9a8e387a816d5dbdff11255e5e8a9d1539 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 8 Dec 2020 22:14:55 +0000 Subject: [PATCH 069/302] Update pyarlo to 0.2.4 (#44034) --- homeassistant/components/arlo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 41d4fc40e5..f046f84f94 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -2,7 +2,7 @@ "domain": "arlo", "name": "Arlo", "documentation": "https://www.home-assistant.io/integrations/arlo", - "requirements": ["pyarlo==0.2.3"], + "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 62baa006c9..2bf1f3b3ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1271,7 +1271,7 @@ pyairvisual==5.0.4 pyalmond==0.0.2 # homeassistant.components.arlo -pyarlo==0.2.3 +pyarlo==0.2.4 # homeassistant.components.atag pyatag==0.3.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02ecc56b6a..a09f961626 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ pyairvisual==5.0.4 pyalmond==0.0.2 # homeassistant.components.arlo -pyarlo==0.2.3 +pyarlo==0.2.4 # homeassistant.components.atag pyatag==0.3.4.4 From 0fc33bd6bf49f5c607d43bddfe46aa4d335aed8d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 9 Dec 2020 00:03:07 +0000 Subject: [PATCH 070/302] [ci skip] Translation update --- homeassistant/components/gios/translations/cs.json | 5 +++++ homeassistant/components/hyperion/translations/cs.json | 8 ++++++++ homeassistant/components/ozw/translations/cs.json | 3 +++ homeassistant/components/tuya/translations/cs.json | 3 +++ homeassistant/components/tuya/translations/en.json | 3 +++ homeassistant/components/tuya/translations/et.json | 3 +++ homeassistant/components/tuya/translations/pl.json | 3 +++ homeassistant/components/tuya/translations/ru.json | 3 +++ 8 files changed, 31 insertions(+) diff --git a/homeassistant/components/gios/translations/cs.json b/homeassistant/components/gios/translations/cs.json index 9a552502af..8dea1f5e01 100644 --- a/homeassistant/components/gios/translations/cs.json +++ b/homeassistant/components/gios/translations/cs.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (polsk\u00fd hlavn\u00ed inspektor\u00e1t ochrany \u017eivotn\u00edho prost\u0159ed\u00ed)" } } + }, + "system_health": { + "info": { + "can_reach_server": "GIOS server dosa\u017een" + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/cs.json b/homeassistant/components/hyperion/translations/cs.json index c5358988ba..46f68baeb1 100644 --- a/homeassistant/components/hyperion/translations/cs.json +++ b/homeassistant/components/hyperion/translations/cs.json @@ -10,6 +10,14 @@ "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token" }, "step": { + "auth": { + "data": { + "create_token": "Automaticky vytvo\u0159it nov\u00fd token" + } + }, + "create_token_external": { + "title": "P\u0159ijmout nov\u00fd token v u\u017eivatelsk\u00e9m rozhran\u00ed Hyperion" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/ozw/translations/cs.json b/homeassistant/components/ozw/translations/cs.json index 4ba465a314..d479efdf95 100644 --- a/homeassistant/components/ozw/translations/cs.json +++ b/homeassistant/components/ozw/translations/cs.json @@ -16,6 +16,9 @@ "hassio_confirm": { "title": "Nastaven\u00ed integrace OpenZWave s dopl\u0148kem OpenZWave" }, + "install_addon": { + "title": "Instalace dopl\u0148ku OpenZWave byla zah\u00e1jena." + }, "on_supervisor": { "data": { "use_addon": "Pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor" diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json index 99cf4be4ff..1dda4ea6df 100644 --- a/homeassistant/components/tuya/translations/cs.json +++ b/homeassistant/components/tuya/translations/cs.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "error": { "dev_multi_type": "V\u00edce vybran\u00fdch za\u0159\u00edzen\u00ed k nastaven\u00ed mus\u00ed b\u00fdt stejn\u00e9ho typu", "dev_not_config": "Typ za\u0159\u00edzen\u00ed nelze nastavit", diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 75c84a5337..46756b18cb 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, "error": { "dev_multi_type": "Multiple selected devices to configure must be of the same type", "dev_not_config": "Device type not configurable", diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 6031af445a..967b38cdb8 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus" + }, "error": { "dev_multi_type": "Mitu h\u00e4\u00e4lestatavat seadet peavad olema sama t\u00fc\u00fcpi", "dev_not_config": "Seda t\u00fc\u00fcpi seade pole seadistatav", diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index ba4810c3f9..a24c1dbe26 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, "error": { "dev_multi_type": "Wybrane urz\u0105dzenia do skonfigurowania musz\u0105 by\u0107 tego samego typu", "dev_not_config": "Typ urz\u0105dzenia nie jest konfigurowalny", diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index 31e2791c9f..b98c6c8e9c 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, "error": { "dev_multi_type": "\u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430.", "dev_not_config": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", From 12903f9c8cf319600142e65cec2b3564df6b3727 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Wed, 9 Dec 2020 16:13:20 +0000 Subject: [PATCH 071/302] =?UTF-8?q?Bump=20openwebifpy=20version:=203.1.1?= =?UTF-8?q?=20=E2=86=92=203.1.6=20(#44064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/enigma2/manifest.json | 2 +- homeassistant/components/enigma2/media_player.py | 5 +++++ requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 86b0614897..fe461654ec 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -2,6 +2,6 @@ "domain": "enigma2", "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", - "requirements": ["openwebifpy==3.1.1"], + "requirements": ["openwebifpy==3.1.6"], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 8bb0486cd2..4baa6aaf04 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -126,6 +126,11 @@ class Enigma2Device(MediaPlayerEntity): """Return the name of the device.""" return self._name + @property + def unique_id(self): + """Return the unique ID for this entity.""" + return self.e2_box.mac_address + @property def state(self): """Return the state of the device.""" diff --git a/requirements_all.txt b/requirements_all.txt index 2bf1f3b3ee..2f7e85ca0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1061,7 +1061,7 @@ openhomedevice==0.7.2 opensensemap-api==0.1.5 # homeassistant.components.enigma2 -openwebifpy==3.1.1 +openwebifpy==3.1.6 # homeassistant.components.luci openwrt-luci-rpc==1.1.6 From dd0afc3b66b8e3826e31a72ac7566d9e62481c41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Dec 2020 10:18:57 -0600 Subject: [PATCH 072/302] Create httpx helper to wrap a shared httpx.AsyncClient (#43877) Co-authored-by: Paulus Schoutsen --- homeassistant/components/pvoutput/sensor.py | 20 ++- .../components/rest/binary_sensor.py | 8 +- homeassistant/components/rest/data.py | 13 +- homeassistant/components/rest/sensor.py | 15 +- homeassistant/components/scrape/sensor.py | 14 +- homeassistant/helpers/httpx_client.py | 88 +++++++++++ tests/components/rest/test_sensor.py | 13 ++ tests/helpers/test_httpx_client.py | 143 ++++++++++++++++++ 8 files changed, 284 insertions(+), 30 deletions(-) create mode 100644 homeassistant/helpers/httpx_client.py create mode 100644 tests/helpers/test_httpx_client.py diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index fb3446fb65..32d33f19e8 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_NAME, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -53,14 +54,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= verify_ssl = DEFAULT_VERIFY_SSL headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} - rest = RestData(method, _ENDPOINT, auth, headers, None, payload, verify_ssl) + rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() if rest.data is None: _LOGGER.error("Unable to fetch data from PVOutput") return False - async_add_entities([PvoutputSensor(rest, name)], True) + async_add_entities([PvoutputSensor(rest, name)]) class PvoutputSensor(Entity): @@ -114,13 +115,18 @@ class PvoutputSensor(Entity): async def async_update(self): """Get the latest data from the PVOutput API and updates the state.""" + await self.rest.async_update() + self._async_update_from_rest_data() + + async def async_added_to_hass(self): + """Ensure the data from the initial update is reflected in the state.""" + self._async_update_from_rest_data() + + @callback + def _async_update_from_rest_data(self): + """Update state from the rest data.""" try: - await self.rest.async_update() self.pvcoutput = self.status._make(self.rest.data.split(",")) except TypeError: self.pvcoutput = None _LOGGER.error("Unable to fetch data from PVOutput. %s", self.rest.data) - - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 7f0f920b84..49c10354c5 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -101,9 +101,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= auth = None rest = RestData( - method, resource, auth, headers, params, payload, verify_ssl, timeout + hass, method, resource, auth, headers, params, payload, verify_ssl, timeout ) await rest.async_update() + if rest.data is None: raise PlatformNotReady @@ -119,7 +120,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= resource_template, ) ], - True, ) @@ -187,10 +187,6 @@ class RestBinarySensor(BinarySensorEntity): """Force update.""" return self._force_update - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() - async def async_update(self): """Get the latest data from REST API and updates the state.""" if self._resource_template is not None: diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index bd35383e98..dd2e29616c 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -3,6 +3,8 @@ import logging import httpx +from homeassistant.helpers.httpx_client import get_async_client + DEFAULT_TIMEOUT = 10 _LOGGER = logging.getLogger(__name__) @@ -13,6 +15,7 @@ class RestData: def __init__( self, + hass, method, resource, auth, @@ -23,6 +26,7 @@ class RestData: timeout=DEFAULT_TIMEOUT, ): """Initialize the data object.""" + self._hass = hass self._method = method self._resource = resource self._auth = auth @@ -35,11 +39,6 @@ class RestData: self.data = None self.headers = None - async def async_remove(self): - """Destroy the http session on destroy.""" - if self._async_client: - await self._async_client.aclose() - def set_url(self, url): """Set url.""" self._resource = url @@ -47,7 +46,9 @@ class RestData: async def async_update(self): """Get the latest data from REST service with provided method.""" if not self._async_client: - self._async_client = httpx.AsyncClient(verify=self._verify_ssl) + self._async_client = get_async_client( + self._hass, verify_ssl=self._verify_ssl + ) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index f048eaa3b4..51d9ec2047 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -116,8 +116,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= else: auth = None rest = RestData( - method, resource, auth, headers, params, payload, verify_ssl, timeout + hass, method, resource, auth, headers, params, payload, verify_ssl, timeout ) + await rest.async_update() if rest.data is None: @@ -140,7 +141,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= json_attrs_path, ) ], - True, ) @@ -210,7 +210,14 @@ class RestSensor(Entity): self.rest.set_url(self._resource_template.async_render(parse_result=False)) await self.rest.async_update() + self._update_from_rest_data() + async def async_added_to_hass(self): + """Ensure the data from the initial update is reflected in the state.""" + self._update_from_rest_data() + + def _update_from_rest_data(self): + """Update state from the rest data.""" value = self.rest.data _LOGGER.debug("Data fetched from resource: %s", value) if self.rest.headers is not None: @@ -266,10 +273,6 @@ class RestSensor(Entity): self._state = value - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() - @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b76995fe39..e8b6fcfd2c 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -78,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= auth = HTTPBasicAuth(username, password) else: auth = None - rest = RestData(method, resource, auth, headers, None, payload, verify_ssl) + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) await rest.async_update() if rest.data is None: @@ -137,6 +137,14 @@ class ScrapeSensor(Entity): async def async_update(self): """Get the latest data from the source and updates the state.""" await self.rest.async_update() + await self._async_update_from_rest_data() + + async def async_added_to_hass(self): + """Ensure the data from the initial update is reflected in the state.""" + await self._async_update_from_rest_data() + + async def _async_update_from_rest_data(self): + """Update state from the rest data.""" if self.rest.data is None: _LOGGER.error("Unable to retrieve data for %s", self.name) return @@ -153,7 +161,3 @@ class ScrapeSensor(Entity): ) else: self._state = value - - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py new file mode 100644 index 0000000000..0f1719b388 --- /dev/null +++ b/homeassistant/helpers/httpx_client.py @@ -0,0 +1,88 @@ +"""Helper for httpx.""" +import sys +from typing import Any, Callable, Optional + +import httpx + +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.core import Event, callback +from homeassistant.helpers.frame import warn_use +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass + +DATA_ASYNC_CLIENT = "httpx_async_client" +DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" +SERVER_SOFTWARE = "HomeAssistant/{0} httpx/{1} Python/{2[0]}.{2[1]}".format( + __version__, httpx.__version__, sys.version_info +) +USER_AGENT = "User-Agent" + + +@callback +@bind_hass +def get_async_client( + hass: HomeAssistantType, verify_ssl: bool = True +) -> httpx.AsyncClient: + """Return default httpx AsyncClient. + + This method must be run in the event loop. + """ + key = DATA_ASYNC_CLIENT if verify_ssl else DATA_ASYNC_CLIENT_NOVERIFY + + client: Optional[httpx.AsyncClient] = hass.data.get(key) + + if client is None: + client = hass.data[key] = create_async_httpx_client(hass, verify_ssl) + + return client + + +@callback +def create_async_httpx_client( + hass: HomeAssistantType, + verify_ssl: bool = True, + auto_cleanup: bool = True, + **kwargs: Any, +) -> httpx.AsyncClient: + """Create a new httpx.AsyncClient with kwargs, i.e. for cookies. + + If auto_cleanup is False, the client will be + automatically closed on homeassistant_stop. + + This method must be run in the event loop. + """ + + client = httpx.AsyncClient( + verify=verify_ssl, + headers={USER_AGENT: SERVER_SOFTWARE}, + **kwargs, + ) + + original_aclose = client.aclose + + client.aclose = warn_use( # type: ignore + client.aclose, "closes the Home Assistant httpx client" + ) + + if auto_cleanup: + _async_register_async_client_shutdown(hass, client, original_aclose) + + return client + + +@callback +def _async_register_async_client_shutdown( + hass: HomeAssistantType, + client: httpx.AsyncClient, + original_aclose: Callable[..., Any], +) -> None: + """Register httpx AsyncClient aclose on Home Assistant shutdown. + + This method must be run in the event loop. + """ + + async def _async_close_client(event: Event) -> None: + """Close httpx client.""" + await original_aclose() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_client) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index f378a1fc2a..16d3f8ba0a 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -6,8 +6,10 @@ import httpx import respx from homeassistant import config as hass_config +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY import homeassistant.components.sensor as sensor from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, DATA_MEGABYTES, @@ -151,10 +153,21 @@ async def test_setup_get(hass): } }, ) + await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + assert hass.states.get("sensor.foo").state == "" + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "sensor.foo"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.foo").state == "" + @respx.mock async def test_setup_get_digest_auth(hass): diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py new file mode 100644 index 0000000000..5444cd4643 --- /dev/null +++ b/tests/helpers/test_httpx_client.py @@ -0,0 +1,143 @@ +"""Test the httpx client helper.""" + +import httpx +import pytest + +from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE +import homeassistant.helpers.httpx_client as client + +from tests.async_mock import Mock, patch + + +async def test_get_async_client_with_ssl(hass): + """Test init async client with ssl.""" + client.get_async_client(hass) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient) + + +async def test_get_async_client_without_ssl(hass): + """Test init async client without ssl.""" + client.get_async_client(hass, verify_ssl=False) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY], httpx.AsyncClient) + + +async def test_create_async_httpx_client_with_ssl_and_cookies(hass): + """Test init async client with ssl and cookies.""" + client.get_async_client(hass) + + httpx_client = client.create_async_httpx_client(hass, cookies={"bla": True}) + assert isinstance(httpx_client, httpx.AsyncClient) + assert hass.data[client.DATA_ASYNC_CLIENT] != httpx_client + + +async def test_create_async_httpx_client_without_ssl_and_cookies(hass): + """Test init async client without ssl and cookies.""" + client.get_async_client(hass, verify_ssl=False) + + httpx_client = client.create_async_httpx_client( + hass, verify_ssl=False, cookies={"bla": True} + ) + assert isinstance(httpx_client, httpx.AsyncClient) + assert hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY] != httpx_client + + +async def test_get_async_client_cleanup(hass): + """Test init async client with ssl.""" + client.get_async_client(hass) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + + assert hass.data[client.DATA_ASYNC_CLIENT].is_closed + + +async def test_get_async_client_cleanup_without_ssl(hass): + """Test init async client without ssl.""" + client.get_async_client(hass, verify_ssl=False) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY], httpx.AsyncClient) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + + assert hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY].is_closed + + +async def test_get_async_client_patched_close(hass): + """Test closing the async client does not work.""" + + with patch("httpx.AsyncClient.aclose") as mock_aclose: + httpx_session = client.get_async_client(hass) + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient) + + with pytest.raises(RuntimeError): + await httpx_session.aclose() + + assert mock_aclose.call_count == 0 + + +async def test_warning_close_session_integration(hass, caplog): + """Test log warning message when closing the session from integration context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="await session.aclose()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + httpx_session = client.get_async_client(hass) + await httpx_session.aclose() + + assert ( + "Detected integration that closes the Home Assistant httpx client. " + "Please report issue for hue using this method at " + "homeassistant/components/hue/light.py, line 23: await session.aclose()" + ) in caplog.text + + +async def test_warning_close_session_custom(hass, caplog): + """Test log warning message when closing the session from custom context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="await session.aclose()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + httpx_session = client.get_async_client(hass) + await httpx_session.aclose() + assert ( + "Detected integration that closes the Home Assistant httpx client. " + "Please report issue to the custom component author for hue using this method at " + "custom_components/hue/light.py, line 23: await session.aclose()" in caplog.text + ) From 20c3fdfe1454dbdc845d1c6c33d934caac294ae8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Dec 2020 17:48:16 +0100 Subject: [PATCH 073/302] Fix ignored Axis config entries doesn't break set up of new entries (#44062) --- homeassistant/components/axis/config_flow.py | 3 ++- tests/components/axis/test_config_flow.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index ea1db54855..8d52b7f8d9 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -5,6 +5,7 @@ from ipaddress import ip_address import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -122,7 +123,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) - if entry.data[CONF_MODEL] == model + if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model ] name = model diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 24f888ea6e..b9dceec747 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.axis.const import ( DEFAULT_STREAM_PROFILE, DOMAIN as AXIS_DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -31,6 +31,8 @@ from tests.common import MockConfigEntry async def test_flow_manual_configuration(hass): """Test that config flow works.""" + MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} ) From e2ef9d1afce16326a456c7f4282da5b9942d8681 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Dec 2020 19:02:44 +0100 Subject: [PATCH 074/302] Some lights only support hs, like the lidl christmas lights (#44059) --- homeassistant/components/deconz/light.py | 16 ++++++-- tests/components/deconz/test_light.py | 52 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index be967a76fe..6d759ccaf4 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -114,7 +114,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP - if self._device.xy is not None: + if self._device.xy is not None or ( + self._device.hue is not None and self._device.sat is not None + ): self._features |= SUPPORT_COLOR if self._device.effect is not None: @@ -141,8 +143,10 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def hs_color(self): """Return the hs color value.""" - if self._device.colormode in ("xy", "hs") and self._device.xy: - return color_util.color_xy_to_hs(*self._device.xy) + if self._device.colormode in ("xy", "hs"): + if self._device.xy: + return color_util.color_xy_to_hs(*self._device.xy) + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) return None @property @@ -163,7 +167,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + if self._device.xy is not None: + data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + else: + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) if ATTR_BRIGHTNESS in kwargs: data["bri"] = kwargs[ATTR_BRIGHTNESS] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index b971de28d4..18a135a5e0 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -353,3 +353,55 @@ async def test_configuration_tool(hass): await setup_deconz_integration(hass, get_state_response=data) assert len(hass.states.async_all()) == 0 + + +async def test_lidl_christmas_light(hass): + """Test that lights or groups entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = { + "0": { + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + xmas_light_device = gateway.api.lights["0"] + + assert len(hass.states.async_all()) == 1 + + with patch.object(xmas_light_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.xmas_light", + ATTR_HS_COLOR: (20, 30), + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", + "/lights/0/state", + json={"on": True, "hue": 3640, "sat": 76}, + ) + + assert hass.states.get("light.xmas_light") From 848224262ceea0e768e7ed54027daa2e6dae696e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 10 Dec 2020 00:03:01 +0000 Subject: [PATCH 075/302] [ci skip] Translation update --- .../components/abode/translations/ca.json | 17 ++++- .../components/abode/translations/pt.json | 3 +- .../accuweather/translations/ca.json | 6 ++ .../accuweather/translations/fr.json | 5 ++ .../components/adguard/translations/pt.json | 3 + .../components/agent_dvr/translations/pt.json | 1 + .../components/airly/translations/ca.json | 5 ++ .../components/airly/translations/fr.json | 5 ++ .../components/airly/translations/pt.json | 3 + .../components/almond/translations/pt.json | 2 + .../ambiclimate/translations/pt.json | 3 + .../ambient_station/translations/pt.json | 3 + .../components/apple_tv/translations/ca.json | 64 ++++++++++++++++++ .../components/apple_tv/translations/cs.json | 1 + .../components/apple_tv/translations/fr.json | 51 ++++++++++++++ .../components/august/translations/pt.json | 9 +++ .../components/avri/translations/pt.json | 3 + .../components/awair/translations/pt.json | 3 +- .../components/axis/translations/pt.json | 1 + .../components/blebox/translations/pt.json | 4 ++ .../components/blink/translations/pt.json | 6 +- .../components/brother/translations/pt.json | 8 +++ .../components/bsblan/translations/ca.json | 4 +- .../components/bsblan/translations/fr.json | 4 +- .../components/bsblan/translations/pt.json | 3 + .../cert_expiry/translations/pt.json | 3 + .../coronavirus/translations/pt.json | 14 ++++ .../components/deconz/translations/pt.json | 15 +++++ .../components/denonavr/translations/pt.json | 4 ++ .../components/directv/translations/pt.json | 4 ++ .../components/doorbird/translations/pt.json | 4 ++ .../components/dsmr/translations/pt.json | 7 ++ .../components/dunehd/translations/pt.json | 3 +- .../components/elgato/translations/pt.json | 1 + .../components/elkm1/translations/pt.json | 3 + .../flick_electric/translations/pt.json | 5 ++ .../components/flume/translations/pt.json | 5 ++ .../flunearyou/translations/pt.json | 3 + .../forked_daapd/translations/pt.json | 4 ++ .../components/freebox/translations/pt.json | 2 + .../garmin_connect/translations/pt.json | 6 +- .../components/gdacs/translations/pt.json | 3 + .../geonetnz_quakes/translations/pt.json | 7 ++ .../components/gios/translations/ca.json | 5 ++ .../components/gios/translations/pt.json | 6 ++ .../components/glances/translations/pt.json | 9 +++ .../components/goalzero/translations/pt.json | 8 +++ .../components/griddy/translations/pt.json | 4 ++ .../components/guardian/translations/pt.json | 2 + .../hisense_aehw4a1/translations/pt.json | 8 +++ .../home_connect/translations/pt.json | 4 ++ .../components/hue/translations/pt.json | 1 + .../humidifier/translations/sv.json | 7 ++ .../hvv_departures/translations/pt.json | 7 ++ .../components/hyperion/translations/ca.json | 52 +++++++++++++++ .../components/hyperion/translations/fr.json | 12 ++++ .../components/icloud/translations/pt.json | 4 +- .../components/ipma/translations/ca.json | 5 ++ .../components/ipp/translations/pt.json | 4 ++ .../components/isy994/translations/pt.json | 8 +++ .../components/izone/translations/pt.json | 8 +++ .../components/konnected/translations/pt.json | 12 ++++ .../components/kulersky/translations/ca.json | 9 ++- .../components/kulersky/translations/fr.json | 7 ++ .../components/local_ip/translations/pt.json | 6 +- .../logi_circle/translations/pt.json | 5 ++ .../lutron_caseta/translations/pt.json | 11 ++++ .../components/melcloud/translations/pt.json | 2 + .../meteo_france/translations/pt.json | 4 ++ .../components/metoffice/translations/pt.json | 7 ++ .../components/mikrotik/translations/pt.json | 10 ++- .../components/mill/translations/pt.json | 3 + .../mobile_app/translations/ca.json | 2 +- .../components/monoprice/translations/pt.json | 19 ++++++ .../components/myq/translations/pt.json | 1 + .../components/neato/translations/pt.json | 1 + .../components/nest/translations/ca.json | 8 +++ .../components/netatmo/translations/pt.json | 1 + .../nightscout/translations/pt.json | 1 + .../components/notion/translations/pt.json | 3 + .../components/nuheat/translations/pt.json | 5 ++ .../components/nut/translations/pt.json | 4 ++ .../opentherm_gw/translations/pt.json | 10 +++ .../components/openuv/translations/pt.json | 3 + .../components/ozw/translations/ca.json | 11 ++++ .../components/ozw/translations/fr.json | 7 ++ .../panasonic_viera/translations/pt.json | 1 + .../components/pi_hole/translations/pt.json | 6 ++ .../components/plaato/translations/pt.json | 5 ++ .../components/plugwise/translations/pt.json | 3 + .../components/ps4/translations/pt.json | 5 +- .../pvpc_hourly_pricing/translations/pt.json | 7 ++ .../components/rachio/translations/pt.json | 1 + .../rainmachine/translations/pt.json | 3 + .../components/rfxtrx/translations/pt.json | 3 +- .../components/rfxtrx/translations/sv.json | 66 +++++++++++++++++++ .../components/ring/translations/pt.json | 4 ++ .../components/roku/translations/pt.json | 4 ++ .../components/samsungtv/translations/pt.json | 6 ++ .../components/sense/translations/pt.json | 5 ++ .../components/smappee/translations/pt.json | 2 + .../components/sms/translations/pt.json | 8 +++ .../components/solarlog/translations/pt.json | 4 ++ .../components/somfy/translations/pt.json | 13 +++- .../components/sonarr/translations/pt.json | 1 + .../components/songpal/translations/pt.json | 3 + .../components/spotify/translations/ca.json | 5 ++ .../components/spotify/translations/pt.json | 3 + .../synology_dsm/translations/pt.json | 3 + .../components/tado/translations/pt.json | 5 ++ .../tellduslive/translations/pt.json | 1 + .../components/tibber/translations/pt.json | 3 +- .../components/tile/translations/pt.json | 3 + .../components/toon/translations/pt.json | 1 + .../components/tradfri/translations/pt.json | 3 +- .../transmission/translations/pt.json | 4 ++ .../components/tuya/translations/ca.json | 5 +- .../components/tuya/translations/fr.json | 3 + .../components/tuya/translations/no.json | 3 + .../components/tuya/translations/pt.json | 8 ++- .../components/tuya/translations/zh-Hant.json | 3 + .../components/unifi/translations/pt.json | 6 ++ .../components/velbus/translations/pt.json | 11 ++++ .../components/vilfo/translations/pt.json | 8 +++ .../components/vizio/translations/pt.json | 22 ++++++- .../components/wemo/translations/pt.json | 8 +++ .../components/withings/translations/pt.json | 5 ++ .../components/wled/translations/pt.json | 1 + .../xiaomi_aqara/translations/pt.json | 4 ++ .../xiaomi_miio/translations/pt.json | 3 + .../components/zerproc/translations/pt.json | 4 ++ 131 files changed, 868 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/apple_tv/translations/ca.json create mode 100644 homeassistant/components/apple_tv/translations/fr.json create mode 100644 homeassistant/components/coronavirus/translations/pt.json create mode 100644 homeassistant/components/dsmr/translations/pt.json create mode 100644 homeassistant/components/geonetnz_quakes/translations/pt.json create mode 100644 homeassistant/components/hisense_aehw4a1/translations/pt.json create mode 100644 homeassistant/components/humidifier/translations/sv.json create mode 100644 homeassistant/components/hyperion/translations/ca.json create mode 100644 homeassistant/components/hyperion/translations/fr.json create mode 100644 homeassistant/components/izone/translations/pt.json create mode 100644 homeassistant/components/kulersky/translations/fr.json create mode 100644 homeassistant/components/lutron_caseta/translations/pt.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/pt.json create mode 100644 homeassistant/components/rfxtrx/translations/sv.json create mode 100644 homeassistant/components/velbus/translations/pt.json create mode 100644 homeassistant/components/wemo/translations/pt.json diff --git a/homeassistant/components/abode/translations/ca.json b/homeassistant/components/abode/translations/ca.json index 47bfd031c8..1d758bc439 100644 --- a/homeassistant/components/abode/translations/ca.json +++ b/homeassistant/components/abode/translations/ca.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_mfa_code": "Codi MFA inv\u00e0lid" }, "step": { + "mfa": { + "data": { + "mfa_code": "Codi MFA (6 d\u00edgits)" + }, + "title": "Introdueix el codi MFA per a Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix la informaci\u00f3 d'inici de sessi\u00f3 d'Abode." + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index cc0e1ef971..95a5174122 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index e1b95b37e0..9c33637baa 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -31,5 +31,11 @@ "title": "Opcions d'AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor d'Accuweather accessible", + "remaining_requests": "Sol\u00b7licituds permeses restants" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 40cf1ccc0b..8e63820541 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -31,5 +31,10 @@ "title": "Options AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e8s au serveur AccuWeather" + } } } \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index 2ae53598eb..5d8abfc9f5 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/agent_dvr/translations/pt.json b/homeassistant/components/agent_dvr/translations/pt.json index fa5aa3de31..f1ef5ef665 100644 --- a/homeassistant/components/agent_dvr/translations/pt.json +++ b/homeassistant/components/agent_dvr/translations/pt.json @@ -4,6 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 2aba1db84c..95400de23b 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor d'Airly accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 5ac31e130f..98407155f1 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e8s au serveur Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index 7a80a1b350..6ebb22b565 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" }, diff --git a/homeassistant/components/almond/translations/pt.json b/homeassistant/components/almond/translations/pt.json index fa5dc98c8f..44f4923964 100644 --- a/homeassistant/components/almond/translations/pt.json +++ b/homeassistant/components/almond/translations/pt.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, diff --git a/homeassistant/components/ambiclimate/translations/pt.json b/homeassistant/components/ambiclimate/translations/pt.json index bb9215f039..591d8c2fea 100644 --- a/homeassistant/components/ambiclimate/translations/pt.json +++ b/homeassistant/components/ambiclimate/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Conta j\u00e1 configurada", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" } } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/pt.json b/homeassistant/components/ambient_station/translations/pt.json index 56c8b5f718..c67faa25f0 100644 --- a/homeassistant/components/ambient_station/translations/pt.json +++ b/homeassistant/components/ambient_station/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "invalid_key": "Chave de API e/ou chave de aplica\u00e7\u00e3o inv\u00e1lidas", "no_devices": "Nenhum dispositivo encontrado na conta" diff --git a/homeassistant/components/apple_tv/translations/ca.json b/homeassistant/components/apple_tv/translations/ca.json new file mode 100644 index 0000000000..e9cd136720 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ca.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "backoff": "En aquests moments el dispositiu no accepta sol\u00b7licituds de vinculaci\u00f3 (\u00e9s possible que hagis introdu\u00eft un codi PIN inv\u00e0lid massa vegades), torna-ho a provar m\u00e9s tard.", + "device_did_not_pair": "No s'ha fet cap intent d'acabar el proc\u00e9s de vinculaci\u00f3 des del dispositiu.", + "invalid_config": "La configuraci\u00f3 d'aquest dispositiu no est\u00e0 completa. Intenta'l tornar a afegir.", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "unknown": "Error inesperat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_usable_service": "S'ha trobat un dispositiu per\u00f2 no ha pogut identificar cap manera d'establir-hi una connexi\u00f3. Si continues veient aquest missatge, prova d'especificar-ne l'adre\u00e7a IP o reinicia l'Apple TV.", + "unknown": "Error inesperat" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e0s a punt d'afegir l'Apple TV amb nom \"{name}\" a Home Assistant.\n\n **Per completar el proc\u00e9s, \u00e9s possible que hagis d'introduir alguns codis PIN.** \n\n Tingues en compte que *no* pots apagar la teva Apple TV a trav\u00e9s d'aquesta integraci\u00f3. Nom\u00e9s es desactivar\u00e0 el reproductor de Home Assistant.", + "title": "Confirma l'addici\u00f3 de l'Apple TV" + }, + "pair_no_pin": { + "description": "Vinculaci\u00f3 necess\u00e0ria amb el servei `{protocol}`. Per continuar, introdueix el PIN {pin} a la teva Apple TV.", + "title": "Vinculaci\u00f3" + }, + "pair_with_pin": { + "data": { + "pin": "Codi PIN" + }, + "description": "Amb el protocol \"{protocol}\" \u00e9s necess\u00e0ria la vinculaci\u00f3. Introdueix el codi PIN que es mostra en pantalla. Els zeros a l'inici, si n'hi ha, s'han d'ometre; per exemple: introdueix 123 si el codi mostrat \u00e9s 0123.", + "title": "Vinculaci\u00f3" + }, + "reconfigure": { + "description": "Aquesta Apple TV est\u00e0 tenint problemes de connexi\u00f3 i s'ha de tornar a configurar.", + "title": "Reconfiguraci\u00f3 de dispositiu" + }, + "service_problem": { + "description": "S'ha produ\u00eft un problema en la vinculaci\u00f3 protocol \"{protocol}\". S'ignorar\u00e0.", + "title": "No s'ha pogut afegir el servei" + }, + "user": { + "data": { + "device_input": "Dispositiu" + }, + "description": "Comen\u00e7a introduint el nom del dispositiu (per exemple, cuina o dormitori) o l'adre\u00e7a IP de l'Apple TV que vulguis afegir. Si autom\u00e0ticament es troben dispositius a la teva xarxa, es mostra a continuaci\u00f3. \n\n Si no veus el teu dispositiu o tens problemes, prova d'especificar l'adre\u00e7a IP del dispositiu. \n\n {devices}", + "title": "Configuraci\u00f3 d'una nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "No engeguis el dispositiu en iniciar Home Assistant" + }, + "description": "Configuraci\u00f3 dels par\u00e0metres generals del dispositiu" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json index 0314afa1f5..ef392a5a66 100644 --- a/homeassistant/components/apple_tv/translations/cs.json +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "invalid_config": "Nastaven\u00ed tohoto za\u0159\u00edzen\u00ed je ne\u00fapln\u00e9. Zkuste jej p\u0159idat znovu.", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json new file mode 100644 index 0000000000..a55d37ed58 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -0,0 +1,51 @@ +{ + "config": { + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil d\u00e9tect\u00e9 sur le r\u00e9seau", + "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", + "unknown": "Erreur innatendue" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Vous \u00eates sur le point d'ajouter l'Apple TV nomm\u00e9e \u00ab {name} \u00bb \u00e0 Home Assistant. \n\n **Pour terminer le processus, vous devrez peut-\u00eatre saisir plusieurs codes PIN.** \n\n Veuillez noter que vous ne pourrez *pas* \u00e9teindre votre Apple TV avec cette int\u00e9gration. Seul le lecteur multim\u00e9dia de Home Assistant s'\u00e9teint!", + "title": "Confirmer l'ajout d'Apple TV" + }, + "pair_no_pin": { + "description": "L'appairage est requis pour le service ` {protocol} `. Veuillez saisir le code PIN {pin} sur votre Apple TV pour continuer.", + "title": "Appairage" + }, + "pair_with_pin": { + "data": { + "pin": "Code PIN" + } + }, + "reconfigure": { + "title": "Reconfiguration de l'appareil" + }, + "service_problem": { + "description": "Un probl\u00e8me est survenu lors du couplage du protocole \u00ab {protocol} \u00bb. Il sera ignor\u00e9.", + "title": "\u00c9chec de l'ajout du service" + }, + "user": { + "data": { + "device_input": "Appareil" + }, + "description": "Commencez par entrer le nom de l'appareil (par exemple, Cuisine ou Chambre) ou l'adresse IP de l'Apple TV que vous souhaitez ajouter. Si des appareils ont \u00e9t\u00e9 d\u00e9tect\u00e9s automatiquement sur votre r\u00e9seau, ils sont affich\u00e9s ci-dessous. \n\n Si vous ne voyez pas votre appareil ou rencontrez des probl\u00e8mes, essayez de sp\u00e9cifier l'adresse IP de l'appareil. \n\n {devices}", + "title": "Configurer une nouvelle Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N'allumez pas l'appareil lors du d\u00e9marrage de Home Assistant" + }, + "description": "Configurer les param\u00e8tres g\u00e9n\u00e9raux de l'appareil" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/pt.json b/homeassistant/components/august/translations/pt.json index 45461329fc..7daa90fad2 100644 --- a/homeassistant/components/august/translations/pt.json +++ b/homeassistant/components/august/translations/pt.json @@ -1,18 +1,27 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "login_method": "M\u00e9todo de login", "password": "Palavra-passe", "username": "Nome de Utilizador" }, "description": "Se o m\u00e9todo de login for 'email', Nome do utilizador \u00e9 o endere\u00e7o de email. Se o m\u00e9todo de login for 'telefone', Nome do utilizador ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'." + }, + "validation": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o" + } } } } diff --git a/homeassistant/components/avri/translations/pt.json b/homeassistant/components/avri/translations/pt.json index 0b323a55dc..77ddb25fb6 100644 --- a/homeassistant/components/avri/translations/pt.json +++ b/homeassistant/components/avri/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index baf9ce33cd..ea99bbf016 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -6,7 +6,8 @@ "reauth_successful": "Token de Acesso actualizado com sucesso" }, "error": { - "invalid_access_token": "Token de acesso inv\u00e1lido" + "invalid_access_token": "Token de acesso inv\u00e1lido", + "unknown": "Erro inesperado" }, "step": { "reauth": { diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index 2175486703..8ba642263a 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -6,6 +6,7 @@ }, "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index 5a8ebbeea0..9c2be6fd04 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado", "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." }, diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index ed650faa02..76c420a584 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_access_token": "Token de acesso inv\u00e1lido" + "invalid_access_token": "Token de acesso inv\u00e1lido", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "2fa": { diff --git a/homeassistant/components/brother/translations/pt.json b/homeassistant/components/brother/translations/pt.json index d62513ec25..f9c19c6be3 100644 --- a/homeassistant/components/brother/translations/pt.json +++ b/homeassistant/components/brother/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "wrong_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." @@ -10,6 +13,11 @@ "host": "Servidor", "type": "Tipo de impressora" } + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo da impressora" + } } } } diff --git a/homeassistant/components/bsblan/translations/ca.json b/homeassistant/components/bsblan/translations/ca.json index 0cce690257..e217787ba1 100644 --- a/homeassistant/components/bsblan/translations/ca.json +++ b/homeassistant/components/bsblan/translations/ca.json @@ -12,7 +12,9 @@ "data": { "host": "Amfitri\u00f3", "passkey": "String Passkey", - "port": "Port" + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" }, "description": "Configura un dispositiu BSB-Lan per a integrar-lo amb Home Assistant.", "title": "Connexi\u00f3 amb dispositiu BSB-Lan" diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json index d650d6596f..0c54aecdd8 100644 --- a/homeassistant/components/bsblan/translations/fr.json +++ b/homeassistant/components/bsblan/translations/fr.json @@ -12,7 +12,9 @@ "data": { "host": "Nom d'h\u00f4te ou adresse IP", "passkey": "Cha\u00eene de cl\u00e9 d'acc\u00e8s", - "port": "Port" + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" }, "description": "Configurez votre appareil BSB-Lan pour l'int\u00e9grer \u00e0 HomeAssistant.", "title": "Connectez-vous \u00e0 l'appareil BSB-Lan" diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index 47b39d574b..5461f20737 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/cert_expiry/translations/pt.json b/homeassistant/components/cert_expiry/translations/pt.json index 0e2e7eeb9c..9f00493666 100644 --- a/homeassistant/components/cert_expiry/translations/pt.json +++ b/homeassistant/components/cert_expiry/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "connection_timeout": "Tempo excedido a tentar ligar ao servidor.", "resolve_failed": "N\u00e3o \u00e9 possivel resolver o servidor" diff --git a/homeassistant/components/coronavirus/translations/pt.json b/homeassistant/components/coronavirus/translations/pt.json new file mode 100644 index 0000000000..e03867478c --- /dev/null +++ b/homeassistant/components/coronavirus/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "country": "Pa\u00eds" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index b33812fa24..725ce07a1b 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Bridge j\u00e1 est\u00e1 configurada", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_bridges": "Nenhum hub deCONZ descoberto", "not_deconz_bridge": "N\u00e3o \u00e9 uma bridge deCONZ" }, @@ -9,6 +10,10 @@ "no_key": "N\u00e3o foi poss\u00edvel obter uma API Key" }, "step": { + "hassio_confirm": { + "description": "Deseja configurar o Home Assistant para se conectar ao gateway deCONZ fornecido pelo addon Hass.io {addon} ?", + "title": "Gateway Zigbee deCONZ via addon Hass.io" + }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Liga\u00e7\u00e3o com deCONZ" @@ -52,5 +57,15 @@ "remote_falling": "Dispositivo em queda livre", "remote_gyro_activated": "Dispositivo agitado" } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configure a visibilidade dos tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/pt.json b/homeassistant/components/denonavr/translations/pt.json index 4a43bae51b..4a00952aaa 100644 --- a/homeassistant/components/denonavr/translations/pt.json +++ b/homeassistant/components/denonavr/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + }, "step": { "select": { "data": { diff --git a/homeassistant/components/directv/translations/pt.json b/homeassistant/components/directv/translations/pt.json index 96a0956765..7880adf5ff 100644 --- a/homeassistant/components/directv/translations/pt.json +++ b/homeassistant/components/directv/translations/pt.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "unknown": "Erro inesperado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/pt.json b/homeassistant/components/doorbird/translations/pt.json index db021f4afc..ceb6c92004 100644 --- a/homeassistant/components/doorbird/translations/pt.json +++ b/homeassistant/components/doorbird/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json new file mode 100644 index 0000000000..ce8a928727 --- /dev/null +++ b/homeassistant/components/dsmr/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/pt.json b/homeassistant/components/dunehd/translations/pt.json index 188c4a2a6d..7fe3a6078c 100644 --- a/homeassistant/components/dunehd/translations/pt.json +++ b/homeassistant/components/dunehd/translations/pt.json @@ -5,7 +5,8 @@ }, "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." }, "step": { "user": { diff --git a/homeassistant/components/elgato/translations/pt.json b/homeassistant/components/elgato/translations/pt.json index 99a658c7a0..0bf8113cca 100644 --- a/homeassistant/components/elgato/translations/pt.json +++ b/homeassistant/components/elgato/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 2f61dbc37e..48d278ac35 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -1,12 +1,15 @@ { "config": { "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { "password": "Palavra-passe (segura apenas)", + "protocol": "Protocolo", "username": "Nome de utilizador (apenas seguro)." } } diff --git a/homeassistant/components/flick_electric/translations/pt.json b/homeassistant/components/flick_electric/translations/pt.json index 4a071063d4..c2bf0536cc 100644 --- a/homeassistant/components/flick_electric/translations/pt.json +++ b/homeassistant/components/flick_electric/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/flume/translations/pt.json b/homeassistant/components/flume/translations/pt.json index 4a071063d4..c2bf0536cc 100644 --- a/homeassistant/components/flume/translations/pt.json +++ b/homeassistant/components/flume/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/flunearyou/translations/pt.json b/homeassistant/components/flunearyou/translations/pt.json index 83724bb7d0..219446a038 100644 --- a/homeassistant/components/flunearyou/translations/pt.json +++ b/homeassistant/components/flunearyou/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/forked_daapd/translations/pt.json b/homeassistant/components/forked_daapd/translations/pt.json index 3ad6eaad7e..e9b298e14e 100644 --- a/homeassistant/components/forked_daapd/translations/pt.json +++ b/homeassistant/components/forked_daapd/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "unknown_error": "Erro inesperado", "wrong_password": "Senha incorreta." }, "step": { diff --git a/homeassistant/components/freebox/translations/pt.json b/homeassistant/components/freebox/translations/pt.json index 3cd69cf5dd..7eacd09c9d 100644 --- a/homeassistant/components/freebox/translations/pt.json +++ b/homeassistant/components/freebox/translations/pt.json @@ -4,6 +4,8 @@ "already_configured": "Servidor j\u00e1 configurado" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "register_failed": "Falha no registo, por favor tente novamente", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/garmin_connect/translations/pt.json b/homeassistant/components/garmin_connect/translations/pt.json index b3b468ff3e..2d9b2f9e9c 100644 --- a/homeassistant/components/garmin_connect/translations/pt.json +++ b/homeassistant/components/garmin_connect/translations/pt.json @@ -4,6 +4,8 @@ "already_configured": "Conta j\u00e1 configurada" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { @@ -11,7 +13,9 @@ "data": { "password": "Palavra-passe", "username": "Nome de Utilizador" - } + }, + "description": "Introduza as suas credenciais.", + "title": "Garmin Connect" } } } diff --git a/homeassistant/components/gdacs/translations/pt.json b/homeassistant/components/gdacs/translations/pt.json index 98180e1124..250400b6e2 100644 --- a/homeassistant/components/gdacs/translations/pt.json +++ b/homeassistant/components/gdacs/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/translations/pt.json b/homeassistant/components/geonetnz_quakes/translations/pt.json new file mode 100644 index 0000000000..d252c078a2 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ca.json b/homeassistant/components/gios/translations/ca.json index 0f1e0b522e..8150e309b2 100644 --- a/homeassistant/components/gios/translations/ca.json +++ b/homeassistant/components/gios/translations/ca.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor de GIO\u015a accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/pt.json b/homeassistant/components/gios/translations/pt.json index 286cd58dd8..47e36006ad 100644 --- a/homeassistant/components/gios/translations/pt.json +++ b/homeassistant/components/gios/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/glances/translations/pt.json b/homeassistant/components/glances/translations/pt.json index d5f64d59f9..0d8cc552dd 100644 --- a/homeassistant/components/glances/translations/pt.json +++ b/homeassistant/components/glances/translations/pt.json @@ -19,5 +19,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/pt.json b/homeassistant/components/goalzero/translations/pt.json index 5ce246347c..ce945ba68d 100644 --- a/homeassistant/components/goalzero/translations/pt.json +++ b/homeassistant/components/goalzero/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/griddy/translations/pt.json b/homeassistant/components/griddy/translations/pt.json index 0c5c776056..9b067d35f8 100644 --- a/homeassistant/components/griddy/translations/pt.json +++ b/homeassistant/components/griddy/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/guardian/translations/pt.json b/homeassistant/components/guardian/translations/pt.json index b2fce54d6b..91def9afb9 100644 --- a/homeassistant/components/guardian/translations/pt.json +++ b/homeassistant/components/guardian/translations/pt.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/pt.json b/homeassistant/components/hisense_aehw4a1/translations/pt.json new file mode 100644 index 0000000000..7a4274b008 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/pt.json b/homeassistant/components/home_connect/translations/pt.json index 462b746816..eb27f25953 100644 --- a/homeassistant/components/home_connect/translations/pt.json +++ b/homeassistant/components/home_connect/translations/pt.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" }, + "create_entry": { + "default": "Autenticado com sucesso" + }, "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index f573f0645b..8eabbbb08c 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Todos os hubs Philips Hue j\u00e1 est\u00e3o configurados", "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "N\u00e3o foi poss\u00edvel conectar-se ao hub", "discover_timeout": "Nenhum hub Hue descoberto", "no_bridges": "Nenhum hub Philips Hue descoberto", diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json new file mode 100644 index 0000000000..325e9f2e6a --- /dev/null +++ b/homeassistant/components/humidifier/translations/sv.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/pt.json b/homeassistant/components/hvv_departures/translations/pt.json index 45e45ab85f..cbd43a04cf 100644 --- a/homeassistant/components/hvv_departures/translations/pt.json +++ b/homeassistant/components/hvv_departures/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hyperion/translations/ca.json b/homeassistant/components/hyperion/translations/ca.json new file mode 100644 index 0000000000..20222bd0bc --- /dev/null +++ b/homeassistant/components/hyperion/translations/ca.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "auth_new_token_not_granted_error": "El nou token creat no est\u00e0 aprovat a Hyperion UI", + "auth_new_token_not_work_error": "No s'ha pogut autenticar amb el nou token creat", + "auth_required_error": "No s'ha pogut determinar si cal autoritzaci\u00f3", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_id": "La inst\u00e0ncia d'Hyperion Ambilight no ha retornat el seu ID" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid" + }, + "step": { + "auth": { + "data": { + "create_token": "Crea un nou token autom\u00e0ticament", + "token": "O proporciona un token ja existent" + }, + "description": "Configura l'autoritzaci\u00f3 amb el teu servidor Hyperion Ambilight" + }, + "confirm": { + "description": "Vols afegir el seg\u00fcent Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Confirma l'addici\u00f3 del servei Hyperion Ambilight" + }, + "create_token": { + "description": "Selecciona **Envia** a continuaci\u00f3 per sol\u00b7licitar un token d'autenticaci\u00f3 nou. Ser\u00e0s redirigit a la interf\u00edcie d'usuari d'Hyperion perqu\u00e8 puguis aprovar la sol\u00b7licitud. Verifica que l'identificador que es mostra \u00e9s \"{auth_id}\"", + "title": "Crea un nou token d'autenticaci\u00f3 autom\u00e0ticament" + }, + "create_token_external": { + "title": "Accepta el nou token a la IU d'Hyperion" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Prioritat Hyperion a utilitzar per als colors i efectes" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json new file mode 100644 index 0000000000..8c1cb919d1 --- /dev/null +++ b/homeassistant/components/hyperion/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index 3a3a13b91c..3e8e4cce2b 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { @@ -22,7 +23,8 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Email" + "username": "Email", + "with_family": "Com a fam\u00edlia" } } } diff --git a/homeassistant/components/ipma/translations/ca.json b/homeassistant/components/ipma/translations/ca.json index 2318a5eba0..806b5aebc6 100644 --- a/homeassistant/components/ipma/translations/ca.json +++ b/homeassistant/components/ipma/translations/ca.json @@ -15,5 +15,10 @@ "title": "Ubicaci\u00f3" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint de l'API d'IPMA accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/pt.json b/homeassistant/components/ipp/translations/pt.json index eccf48139b..1f312c187c 100644 --- a/homeassistant/components/ipp/translations/pt.json +++ b/homeassistant/components/ipp/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o", "ipp_error": "Erro IPP encontrado.", "ipp_version_error": "Vers\u00e3o IPP n\u00e3o suportada pela impressora." @@ -16,6 +17,9 @@ "ssl": "Utiliza um certificado SSL", "verify_ssl": "Verificar o certificado SSL" } + }, + "zeroconf_confirm": { + "title": "Impressora encontrada" } } } diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index fe44091317..3696210051 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/izone/translations/pt.json b/homeassistant/components/izone/translations/pt.json new file mode 100644 index 0000000000..7a4274b008 --- /dev/null +++ b/homeassistant/components/izone/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index 19ddbf6057..64aaf6cbf4 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { @@ -10,6 +18,10 @@ } }, "options": { + "error": { + "one": "Vazio", + "other": "Vazios" + }, "step": { "options_binary": { "data": { diff --git a/homeassistant/components/kulersky/translations/ca.json b/homeassistant/components/kulersky/translations/ca.json index 7d765f80f8..dc21c371e6 100644 --- a/homeassistant/components/kulersky/translations/ca.json +++ b/homeassistant/components/kulersky/translations/ca.json @@ -1,8 +1,13 @@ { "config": { "abort": { - "no_devices_found": "No s'ha trobat cap dispositiu a la xarxa", - "single_instance_allowed": "Ja est\u00e0 configurat. Nom\u00e9s una configuraci\u00f3 \u00e9s possible" + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json new file mode 100644 index 0000000000..4c984a5569 --- /dev/null +++ b/homeassistant/components/kulersky/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/pt.json b/homeassistant/components/local_ip/translations/pt.json index ef31de296c..c5a4032636 100644 --- a/homeassistant/components/local_ip/translations/pt.json +++ b/homeassistant/components/local_ip/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { "user": { "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" } } - } + }, + "title": "Endere\u00e7o IP Local" } \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/pt.json b/homeassistant/components/logi_circle/translations/pt.json index 9a0e75c24f..3837580154 100644 --- a/homeassistant/components/logi_circle/translations/pt.json +++ b/homeassistant/components/logi_circle/translations/pt.json @@ -7,6 +7,11 @@ "error": { "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "title": "Provedor de autentica\u00e7\u00e3o" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json new file mode 100644 index 0000000000..a04f550a71 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/pt.json b/homeassistant/components/melcloud/translations/pt.json index 25623dc04a..67f59434d4 100644 --- a/homeassistant/components/melcloud/translations/pt.json +++ b/homeassistant/components/melcloud/translations/pt.json @@ -4,6 +4,8 @@ "already_configured": "Integra\u00e7\u00e3o com o MELCloud j\u00e1 configurada para este email. O token de acesso foi atualizado." }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/meteo_france/translations/pt.json b/homeassistant/components/meteo_france/translations/pt.json index 025d58f519..f53975ecf0 100644 --- a/homeassistant/components/meteo_france/translations/pt.json +++ b/homeassistant/components/meteo_france/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/metoffice/translations/pt.json b/homeassistant/components/metoffice/translations/pt.json index 7a8b164e32..d974101d0a 100644 --- a/homeassistant/components/metoffice/translations/pt.json +++ b/homeassistant/components/metoffice/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mikrotik/translations/pt.json b/homeassistant/components/mikrotik/translations/pt.json index b177591d4e..72d275069c 100644 --- a/homeassistant/components/mikrotik/translations/pt.json +++ b/homeassistant/components/mikrotik/translations/pt.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "name_exists": "Nome existe" }, "step": { "user": { @@ -10,7 +15,8 @@ "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "username": "Nome de Utilizador", + "verify_ssl": "Utilizar SSL" } } } diff --git a/homeassistant/components/mill/translations/pt.json b/homeassistant/components/mill/translations/pt.json index f1e1a2f9fe..4348cecf5c 100644 --- a/homeassistant/components/mill/translations/pt.json +++ b/homeassistant/components/mill/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index 4e857279e9..a36fd1ca13 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -11,7 +11,7 @@ }, "device_automation": { "action_type": { - "notify": "Enviar una notificaci\u00f3" + "notify": "Envia una notificaci\u00f3" } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/pt.json b/homeassistant/components/monoprice/translations/pt.json index ccc0fc7c47..d73c17a62c 100644 --- a/homeassistant/components/monoprice/translations/pt.json +++ b/homeassistant/components/monoprice/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { @@ -10,5 +14,20 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nome da fonte #1", + "source_2": "Nome da fonte #2", + "source_3": "Nome da fonte #3", + "source_4": "Nome da fonte #4", + "source_5": "Nome da fonte #5", + "source_6": "Nome da fonte #6" + }, + "title": "Configurar fontes" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/myq/translations/pt.json b/homeassistant/components/myq/translations/pt.json index b47060ac87..14f1703524 100644 --- a/homeassistant/components/myq/translations/pt.json +++ b/homeassistant/components/myq/translations/pt.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index 809cbc360a..0672c9af33 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "error": { diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 46558f2e89..daf07ea445 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -36,5 +36,13 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Moviment detectat", + "camera_person": "Persona detectada", + "camera_sound": "So detectat", + "doorbell_chime": "Timbre premut" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index afab5e616e..e39ecffa8a 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json index f2766f5a2c..093b777582 100644 --- a/homeassistant/components/nightscout/translations/pt.json +++ b/homeassistant/components/nightscout/translations/pt.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/notion/translations/pt.json b/homeassistant/components/notion/translations/pt.json index c0a3dae8aa..e92d51b205 100644 --- a/homeassistant/components/notion/translations/pt.json +++ b/homeassistant/components/notion/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/nuheat/translations/pt.json b/homeassistant/components/nuheat/translations/pt.json index 4a071063d4..7953cf5625 100644 --- a/homeassistant/components/nuheat/translations/pt.json +++ b/homeassistant/components/nuheat/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nut/translations/pt.json b/homeassistant/components/nut/translations/pt.json index 5edf0b18dd..a856ef0aee 100644 --- a/homeassistant/components/nut/translations/pt.json +++ b/homeassistant/components/nut/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json index 4285ee45c8..85b6e61796 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { @@ -11,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "precision": "Precis\u00e3o" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/pt.json b/homeassistant/components/openuv/translations/pt.json index c408b4bec3..6433111fe8 100644 --- a/homeassistant/components/openuv/translations/pt.json +++ b/homeassistant/components/openuv/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" }, diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json index 6010da17b7..4553589e36 100644 --- a/homeassistant/components/ozw/translations/ca.json +++ b/homeassistant/components/ozw/translations/ca.json @@ -4,13 +4,24 @@ "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement OpenZWave.", "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement OpenZWave.", "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 d'OpenZWave.", + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { "addon_start_failed": "No s'ha pogut iniciar el complement OpenZWave. Comprova la configuraci\u00f3." }, + "progress": { + "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement OpenZWave. Pot tardar uns quants minuts." + }, "step": { + "hassio_confirm": { + "title": "Configuraci\u00f3 de la integraci\u00f3 d'OpenZWave amb el complement OpenZWave" + }, + "install_addon": { + "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "Utilitza el complement OpenZWave Supervisor" diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json index cbf9bfb6bd..c4ea835d86 100644 --- a/homeassistant/components/ozw/translations/fr.json +++ b/homeassistant/components/ozw/translations/fr.json @@ -4,13 +4,20 @@ "addon_info_failed": "Impossible d\u2019obtenir des informations de l'add-on OpenZWave.", "addon_install_failed": "\u00c9chec de l\u2019installation de l'add-on OpenZWave.", "addon_set_config_failed": "\u00c9chec de la configuration OpenZWave.", + "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9", "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "addon_start_failed": "\u00c9chec du d\u00e9marrage de l'add-on OpenZWave. V\u00e9rifiez la configuration." }, + "progress": { + "install_addon": "Veuillez patienter pendant que l'installation du module OpenZWave se termine. Cela peut prendre plusieurs minutes." + }, "step": { + "hassio_confirm": { + "title": "Configurer l\u2019int\u00e9gration OpenZWave avec l\u2019add-on OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "Utiliser l'add-on OpenZWave Supervisor" diff --git a/homeassistant/components/panasonic_viera/translations/pt.json b/homeassistant/components/panasonic_viera/translations/pt.json index 9a5c19acf1..411d3a8610 100644 --- a/homeassistant/components/panasonic_viera/translations/pt.json +++ b/homeassistant/components/panasonic_viera/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index 6e0b2481c8..e56597a400 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index 3d0630027a..e9890abba2 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -3,6 +3,11 @@ "abort": { "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json index 65283f66f4..dd40927a7c 100644 --- a/homeassistant/components/plugwise/translations/pt.json +++ b/homeassistant/components/plugwise/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index 51ea84f7c2..5956937ac2 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -3,10 +3,13 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "credential_error": "Erro ao obter credenciais.", - "no_devices_found": "N\u00e3o foram encontrados dispositivos PlayStation 4 na rede." + "no_devices_found": "N\u00e3o foram encontrados dispositivos PlayStation 4 na rede.", + "port_987_bind_error": "N\u00e3o foi poss\u00edvel ligar-se \u00e0 porta 987. Consulte a [documenta\u00e7\u00e3o](https://www.home-assistant.io/components/ps4/) para obter mais informa\u00e7\u00f5es.", + "port_997_bind_error": "N\u00e3o foi poss\u00edvel ligar-se \u00e0 porta 997. Consulte a [documenta\u00e7\u00e3o](https://www.home-assistant.io/components/ps4/) para obter mais informa\u00e7\u00f5es." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "credential_timeout": "O servi\u00e7o de credencial expirou. Pressione enviar para reiniciar.", "login_failed": "Falha ao emparelhar com a PlayStation 4. Verifique se o PIN est\u00e1 correto." }, "step": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json new file mode 100644 index 0000000000..d252c078a2 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json index a5648a9138..626909b9b2 100644 --- a/homeassistant/components/rachio/translations/pt.json +++ b/homeassistant/components/rachio/translations/pt.json @@ -4,6 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/rainmachine/translations/pt.json b/homeassistant/components/rainmachine/translations/pt.json index 99cd4dba4e..1102078fc6 100644 --- a/homeassistant/components/rainmachine/translations/pt.json +++ b/homeassistant/components/rainmachine/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/rfxtrx/translations/pt.json b/homeassistant/components/rfxtrx/translations/pt.json index 335e097e99..c962097e6a 100644 --- a/homeassistant/components/rfxtrx/translations/pt.json +++ b/homeassistant/components/rfxtrx/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json new file mode 100644 index 0000000000..bdafd86c9a --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -0,0 +1,66 @@ +{ + "config": { + "step": { + "setup_network": { + "data": { + "host": "V\u00e4rdnamn", + "port": "Port" + }, + "title": "V\u00e4lj anslutningsadress" + }, + "setup_serial": { + "data": { + "device": "V\u00e4lj enhet" + }, + "title": "Enhet" + }, + "setup_serial_manual_path": { + "data": { + "device": "USB-s\u00f6kv\u00e4g" + }, + "title": "S\u00f6kv\u00e4g" + }, + "user": { + "data": { + "type": "Anslutningstyp" + }, + "title": "V\u00e4lj anslutningstyp" + } + } + }, + "options": { + "error": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "invalid_event_code": "Ogiltig h\u00e4ndelsekod", + "invalid_input_2262_off": "Ogiltig v\u00e4rde f\u00f6r av-kommando", + "invalid_input_2262_on": "Ogiltig v\u00e4rde f\u00f6r p\u00e5-kommando", + "invalid_input_off_delay": "Ogiltigt v\u00e4rde f\u00f6r avst\u00e4ngningsf\u00f6rdr\u00f6jning", + "unknown": "Ok\u00e4nt fel" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "Aktivera automatisk till\u00e4gg av enheter", + "debug": "Aktivera fels\u00f6kning", + "device": "V\u00e4lj enhet att konfigurera", + "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till", + "remove_device": "V\u00e4lj enhet som ska tas bort" + }, + "title": "Rfxtrx-alternativ" + }, + "set_device_options": { + "data": { + "command_off": "Databitv\u00e4rde f\u00f6r av-kommando", + "command_on": "Databitv\u00e4rde f\u00f6r p\u00e5-kommando", + "data_bit": "Antal databitar", + "fire_event": "Aktivera enhetsh\u00e4ndelse", + "off_delay": "Avst\u00e4ngningsf\u00f6rdr\u00f6jning", + "off_delay_enabled": "Aktivera avst\u00e4ngningsf\u00f6rdr\u00f6jning", + "replace_device": "V\u00e4lj enhet att ers\u00e4tta", + "signal_repetitions": "Antal signalrepetitioner" + }, + "title": "Konfigurera enhetsalternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/pt.json b/homeassistant/components/ring/translations/pt.json index 4a071063d4..0918f2cca1 100644 --- a/homeassistant/components/ring/translations/pt.json +++ b/homeassistant/components/ring/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/roku/translations/pt.json b/homeassistant/components/roku/translations/pt.json index 7880adf5ff..e67de50945 100644 --- a/homeassistant/components/roku/translations/pt.json +++ b/homeassistant/components/roku/translations/pt.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "Roku: {name}", "step": { + "ssdp_confirm": { + "title": "Roku" + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index 8493f66031..15e61d2362 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -1,9 +1,15 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "TV Samsung: {model}", "step": { + "confirm": { + "title": "TV Samsung" + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index 196be985b6..e3b78cd8e4 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/smappee/translations/pt.json b/homeassistant/components/smappee/translations/pt.json index 54f841b224..75c24278a8 100644 --- a/homeassistant/components/smappee/translations/pt.json +++ b/homeassistant/components/smappee/translations/pt.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" }, "step": { diff --git a/homeassistant/components/sms/translations/pt.json b/homeassistant/components/sms/translations/pt.json index 38544eb2ce..4ccc36bcc2 100644 --- a/homeassistant/components/sms/translations/pt.json +++ b/homeassistant/components/sms/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/solarlog/translations/pt.json b/homeassistant/components/solarlog/translations/pt.json index 88cfc4a797..a37c510a36 100644 --- a/homeassistant/components/solarlog/translations/pt.json +++ b/homeassistant/components/solarlog/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o, por favor verifique o endere\u00e7o do servidor" }, "step": { diff --git a/homeassistant/components/somfy/translations/pt.json b/homeassistant/components/somfy/translations/pt.json index 28b7a920e9..592ccd8558 100644 --- a/homeassistant/components/somfy/translations/pt.json +++ b/homeassistant/components/somfy/translations/pt.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } } } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/pt.json b/homeassistant/components/sonarr/translations/pt.json index 43b4c5d49b..24a1283331 100644 --- a/homeassistant/components/sonarr/translations/pt.json +++ b/homeassistant/components/sonarr/translations/pt.json @@ -9,6 +9,7 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "Sonarr: {name}", "step": { "reauth_confirm": { "title": "Reautenticar integra\u00e7\u00e3o" diff --git a/homeassistant/components/songpal/translations/pt.json b/homeassistant/components/songpal/translations/pt.json index ce8a928727..db0e0c2a13 100644 --- a/homeassistant/components/songpal/translations/pt.json +++ b/homeassistant/components/songpal/translations/pt.json @@ -2,6 +2,9 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json index 0210147a48..fffb248573 100644 --- a/homeassistant/components/spotify/translations/ca.json +++ b/homeassistant/components/spotify/translations/ca.json @@ -18,5 +18,10 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint de l'API d'Spotify accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/pt.json b/homeassistant/components/spotify/translations/pt.json index f3da93ad96..0719e226cd 100644 --- a/homeassistant/components/spotify/translations/pt.json +++ b/homeassistant/components/spotify/translations/pt.json @@ -5,6 +5,9 @@ "reauth_account_mismatch": "A conta Spotify com a qual foi autenticada n\u00e3o corresponde \u00e0 conta necess\u00e1ria para a reautentica\u00e7\u00e3o." }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, "reauth_confirm": { "description": "A integra\u00e7\u00e3o do Spotify precisa ser reautenticada com o Spotify para a conta: {account}", "title": "Reautenticar com Spotify" diff --git a/homeassistant/components/synology_dsm/translations/pt.json b/homeassistant/components/synology_dsm/translations/pt.json index dcc4132df7..9745f897e0 100644 --- a/homeassistant/components/synology_dsm/translations/pt.json +++ b/homeassistant/components/synology_dsm/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", diff --git a/homeassistant/components/tado/translations/pt.json b/homeassistant/components/tado/translations/pt.json index 4a071063d4..7953cf5625 100644 --- a/homeassistant/components/tado/translations/pt.json +++ b/homeassistant/components/tado/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index e8abc7c865..cde0a2ad9c 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido", diff --git a/homeassistant/components/tibber/translations/pt.json b/homeassistant/components/tibber/translations/pt.json index bed3f76be0..941089ee0c 100644 --- a/homeassistant/components/tibber/translations/pt.json +++ b/homeassistant/components/tibber/translations/pt.json @@ -4,7 +4,8 @@ "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido" }, "step": { "user": { diff --git a/homeassistant/components/tile/translations/pt.json b/homeassistant/components/tile/translations/pt.json index e7e5968a6d..bfafaa77b4 100644 --- a/homeassistant/components/tile/translations/pt.json +++ b/homeassistant/components/tile/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/toon/translations/pt.json b/homeassistant/components/toon/translations/pt.json index 189f0c1b2f..e4aaaa3913 100644 --- a/homeassistant/components/toon/translations/pt.json +++ b/homeassistant/components/toon/translations/pt.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, diff --git a/homeassistant/components/tradfri/translations/pt.json b/homeassistant/components/tradfri/translations/pt.json index e4cf0e9787..a92f8d4dbd 100644 --- a/homeassistant/components/tradfri/translations/pt.json +++ b/homeassistant/components/tradfri/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Bridge j\u00e1 est\u00e1 configurada" + "already_configured": "Bridge j\u00e1 est\u00e1 configurada", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" }, "error": { "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar \u00e0 gateway.", diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index c311dbf9ef..c3d4131d99 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "name_exists": "Nome j\u00e1 existe" }, diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index d891eea20a..908cf287ee 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, "error": { "dev_multi_type": "Per configurar una selecci\u00f3 de m\u00faltiples dispositius, aquests han de ser del mateix tipus", "dev_not_config": "El tipus d'aquest dispositiu no \u00e9s configurable", @@ -42,7 +45,7 @@ "tuya_max_coltemp": "Temperatura de color m\u00e0xima enviada pel dispositiu", "unit_of_measurement": "Unitat de temperatura utilitzada pel dispositiu" }, - "description": "Configura les opcions per ajustar la informaci\u00f3 mostrada per {device_type} dispositiu `{device_name}`", + "description": "Configura les opcions per ajustar la informaci\u00f3 mostrada pel dispositiu {device_type} `{device_name}`", "title": "Configuraci\u00f3 de dispositiu Tuya" }, "init": { diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index e25dcb40d4..9ef1c325d1 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Impossible de se connecter" + }, "error": { "dev_multi_type": "Plusieurs p\u00e9riph\u00e9riques s\u00e9lectionn\u00e9s \u00e0 configurer doivent \u00eatre du m\u00eame type", "dev_not_config": "Type d'appareil non configurable", diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index 38f054ae4c..d0c1a3ca18 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Tilkobling mislyktes" + }, "error": { "dev_multi_type": "Flere valgte enheter som skal konfigureres, m\u00e5 v\u00e6re av samme type", "dev_not_config": "Enhetstype kan ikke konfigureres", diff --git a/homeassistant/components/tuya/translations/pt.json b/homeassistant/components/tuya/translations/pt.json index ae7ec8fa5e..566746538c 100644 --- a/homeassistant/components/tuya/translations/pt.json +++ b/homeassistant/components/tuya/translations/pt.json @@ -2,7 +2,8 @@ "config": { "abort": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" @@ -15,5 +16,10 @@ } } } + }, + "options": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index 3735d12e61..08871c3108 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, "error": { "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", diff --git a/homeassistant/components/unifi/translations/pt.json b/homeassistant/components/unifi/translations/pt.json index 354870a0d5..7a0a8e1a1f 100644 --- a/homeassistant/components/unifi/translations/pt.json +++ b/homeassistant/components/unifi/translations/pt.json @@ -41,6 +41,12 @@ "other": "Vazios" } }, + "simple_options": { + "data": { + "track_clients": "Acompanhar clientes da rede", + "track_devices": "Acompanhar dispositivos de rede (dispositivos Ubiquiti)" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Criar sensores de uso de largura de banda para clientes da rede" diff --git a/homeassistant/components/velbus/translations/pt.json b/homeassistant/components/velbus/translations/pt.json new file mode 100644 index 0000000000..94b13c6bc7 --- /dev/null +++ b/homeassistant/components/velbus/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/pt.json b/homeassistant/components/vilfo/translations/pt.json index c30760a9a8..9f7a591855 100644 --- a/homeassistant/components/vilfo/translations/pt.json +++ b/homeassistant/components/vilfo/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/pt.json b/homeassistant/components/vizio/translations/pt.json index dce6f5934d..b8259aca07 100644 --- a/homeassistant/components/vizio/translations/pt.json +++ b/homeassistant/components/vizio/translations/pt.json @@ -1,6 +1,10 @@ { "config": { "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { @@ -9,12 +13,28 @@ "pin": "PIN" } }, + "pairing_complete": { + "description": "O seu Dispositivo VIZIO SmartCast j\u00e1 se encontra ligado ao Home Assistant.", + "title": "Emparelhamento Completo" + }, + "pairing_complete_import": { + "title": "Emparelhamento Completo" + }, "user": { "data": { "access_token": "Token de Acesso", + "device_class": "Tipo de dispositivo", "host": "Servidor", "name": "Nome" - } + }, + "title": "Dispositivo VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "title": "Atualizar op\u00e7\u00f5es de Dispositivo VIZIO SmartCast" } } } diff --git a/homeassistant/components/wemo/translations/pt.json b/homeassistant/components/wemo/translations/pt.json new file mode 100644 index 0000000000..7a4274b008 --- /dev/null +++ b/homeassistant/components/wemo/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index 83caf877b9..1fe7083ecf 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" }, "error": { "already_configured": "Conta j\u00e1 configurada" }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, "profile": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index 849cb1588f..313c9057da 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index a1340f587c..a800e4d57c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + }, "error": { "invalid_host": "Endere\u00e7o IP Inv\u00e1lido" }, diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index cd1c9b7978..65edf2dbe3 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/zerproc/translations/pt.json b/homeassistant/components/zerproc/translations/pt.json index 1424e35f35..e25888655a 100644 --- a/homeassistant/components/zerproc/translations/pt.json +++ b/homeassistant/components/zerproc/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { "confirm": { "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" From b9a3a001fb758f64bd5b0307c3602c3076c32b6a Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 10 Dec 2020 00:41:11 -0800 Subject: [PATCH 076/302] Include Hyperion in coverage testing (#44096) --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 76d4872011..c887fc385e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -386,7 +386,6 @@ omit = homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* - homeassistant/components/hyperion/light.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py From e09234ffae2ffc3f0d0f98d440ec11f72e4fe4bf Mon Sep 17 00:00:00 2001 From: zewelor Date: Thu, 10 Dec 2020 09:54:10 +0100 Subject: [PATCH 077/302] Fix yeelight unavailbility (#44061) --- homeassistant/components/yeelight/__init__.py | 66 ++++++++++++------- tests/components/yeelight/__init__.py | 3 +- tests/components/yeelight/test_init.py | 46 ++++++++++++- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index ae9d75de54..324999c712 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" +DEVICE_INITIALIZED = "yeelight_{}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 @@ -181,8 +181,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" async def _initialize(host: str, capabilities: Optional[dict] = None) -> None: - device = await _async_setup_device(hass, host, entry, capabilities) + async_dispatcher_connect( + hass, + DEVICE_INITIALIZED.format(host), + _load_platforms, + ) + + device = await _async_get_device(hass, host, entry, capabilities) hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device + + await device.async_setup() + + async def _load_platforms(): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -249,28 +260,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def _async_setup_device( - hass: HomeAssistant, - host: str, - entry: ConfigEntry, - capabilities: Optional[dict], -) -> None: - # Get model from config and capabilities - model = entry.options.get(CONF_MODEL) - if not model and capabilities is not None: - model = capabilities.get("model") - - # Set up device - bulb = Bulb(host, model=model or None) - if capabilities is None: - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) - - device = YeelightDevice(hass, host, entry.options, bulb, capabilities) - await hass.async_add_executor_job(device.update) - await device.async_setup() - return device - - @callback def _async_unique_name(capabilities: dict) -> str: """Generate name from capabilities.""" @@ -374,6 +363,7 @@ class YeelightDevice: self._device_type = None self._available = False self._remove_time_tracker = None + self._initialized = False self._name = host # Default name is host if capabilities: @@ -495,6 +485,8 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True + if not self._initialized: + self._initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -522,6 +514,11 @@ class YeelightDevice: ex, ) + def _initialize_device(self): + self._get_capabilities() + self._initialized = True + dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + def update(self): """Update device properties and send data updated signal.""" self._update_properties() @@ -584,3 +581,22 @@ class YeelightEntity(Entity): def update(self) -> None: """Update the entity.""" self._device.update() + + +async def _async_get_device( + hass: HomeAssistant, + host: str, + entry: ConfigEntry, + capabilities: Optional[dict], +) -> YeelightDevice: + # Get model from config and capabilities + model = entry.options.get(CONF_MODEL) + if not model and capabilities is not None: + model = capabilities.get("model") + + # Set up device + bulb = Bulb(host, model=model or None) + if capabilities is None: + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + + return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 9f811586a7..5405b69490 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -55,7 +55,8 @@ PROPERTIES = { "current_brightness": "30", } -ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight" +ENTITY_BINARY_SENSOR_TEMPLATE = "binary_sensor.{}_nightlight" +ENTITY_BINARY_SENSOR = ENTITY_BINARY_SENSOR_TEMPLATE.format(UNIQUE_NAME) ENTITY_LIGHT = f"light.{UNIQUE_NAME}" ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight" ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index d9c23cfa1a..882f9944ca 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,21 +1,27 @@ """Test Yeelight.""" +from unittest.mock import MagicMock + from yeelight import BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, + DATA_CONFIG_ENTRIES, + DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from . import ( + CAPABILITIES, CONFIG_ENTRY_DATA, ENTITY_AMBILIGHT, ENTITY_BINARY_SENSOR, + ENTITY_BINARY_SENSOR_TEMPLATE, ENTITY_LIGHT, ENTITY_NIGHTLIGHT, ID, @@ -115,6 +121,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -132,3 +139,40 @@ async def test_unique_ids_entry(hass: HomeAssistant): assert ( er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight" ) + + +async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): + """Test Yeelight off while adding to ha, for example on HA start.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + **CONFIG_ENTRY_DATA, + CONF_HOST: IP_ADDRESS, + }, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( + IP_ADDRESS.replace(".", "_") + ) + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is None + + type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) + type(mocked_bulb).get_properties = MagicMock(None) + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.async_block_till_done() + + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is not None From b15d92edfdba0062e05900bf5c76374467427726 Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Thu, 10 Dec 2020 01:09:08 -0800 Subject: [PATCH 078/302] Fix transmission torrent filtering and sorting (#44069) --- homeassistant/components/transmission/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ef1e68e2d0..ea62de71e8 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -145,12 +145,10 @@ class TransmissionTorrentsSensor(TransmissionSensor): @property def device_state_attributes(self): """Return the state attributes, if any.""" - limit = self._tm_client.config_entry.options[CONF_LIMIT] - order = self._tm_client.config_entry.options[CONF_ORDER] - torrents = self._tm_client.api.torrents[0:limit] info = _torrents_info( - torrents, - order=order, + torrents=self._tm_client.api.torrents, + order=self._tm_client.config_entry.options[CONF_ORDER], + limit=self._tm_client.config_entry.options[CONF_LIMIT], statuses=self.SUBTYPE_MODES[self._sub_type], ) return { @@ -173,11 +171,11 @@ def _filter_torrents(torrents, statuses=None): ] -def _torrents_info(torrents, order, statuses=None): +def _torrents_info(torrents, order, limit, statuses=None): infos = {} torrents = _filter_torrents(torrents, statuses) torrents = SUPPORTED_ORDER_MODES[order](torrents) - for torrent in _filter_torrents(torrents, statuses): + for torrent in torrents[:limit]: info = infos[torrent.name] = { "added_date": torrent.addedDate, "percent_done": f"{torrent.percentDone * 100:.2f}", From 9cce6e91f19261da3479ca5cbbc30f06878c44a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 10:10:38 +0100 Subject: [PATCH 079/302] Bump hass-nabucasa to 0.39.0 (#44097) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 3f65ed2ba4..03bf276185 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.38.0"], + "requirements": ["hass-nabucasa==0.39.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1818385c0e..5e4bb07394 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==3.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' diff --git a/requirements_all.txt b/requirements_all.txt index 2f7e85ca0b..57dcee73b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a09f961626..262b0e816e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,7 @@ ha-ffmpeg==3.0.2 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 # homeassistant.components.tasmota hatasmota==0.1.4 From d9a86c1145275b60da57a9d19457970d129b4a41 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 11:11:49 +0100 Subject: [PATCH 080/302] Fix importing blueprints from forums with HTML entities (#44098) --- .../components/blueprint/importer.py | 3 +- tests/components/blueprint/test_importer.py | 79 +- tests/fixtures/blueprint/community_post.json | 749 ++++++++---------- 3 files changed, 391 insertions(+), 440 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 524b04293e..f0230aba1b 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -1,5 +1,6 @@ """Import logic for blueprint.""" from dataclasses import dataclass +import html import re from typing import Optional @@ -110,7 +111,7 @@ def _extract_blueprint_from_community_topic( block_content = block_content.strip() try: - data = yaml.parse_yaml(block_content) + data = yaml.parse_yaml(html.unescape(block_content)) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index bb8903459c..8e674e3a9d 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -16,6 +16,70 @@ def community_post(): return load_fixture("blueprint/community_post.json") +COMMUNITY_POST_INPUTS = { + "remote": { + "name": "Remote", + "description": "IKEA remote to use", + "selector": { + "device": { + "integration": "zha", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + } + }, + }, + "light": { + "name": "Light(s)", + "description": "The light(s) to control", + "selector": {"target": {"entity": {"domain": "light"}}}, + }, + "force_brightness": { + "name": "Force turn on brightness", + "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n', + "default": False, + "selector": {"boolean": {}}, + }, + "brightness": { + "name": "Brightness", + "description": "Brightness of the light(s) when turning on", + "default": 50, + "selector": { + "number": { + "min": 0.0, + "max": 100.0, + "mode": "slider", + "step": 1.0, + "unit_of_measurement": "%", + } + }, + }, + "button_left_short": { + "name": "Left button - short press", + "description": "Action to run on short left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_left_long": { + "name": "Left button - long press", + "description": "Action to run on long left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_short": { + "name": "Right button - short press", + "description": "Action to run on short right button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_long": { + "name": "Right button - long press", + "description": "Action to run on long right button press", + "default": [], + "selector": {"action": {}}, + }, +} + + def test_get_community_post_import_url(): """Test variations of generating import forum url.""" assert ( @@ -57,10 +121,7 @@ def test_extract_blueprint_from_community_topic(community_post): ) assert imported_blueprint is not None assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS def test_extract_blueprint_from_community_topic_invalid_yaml(): @@ -103,11 +164,11 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit ) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } - assert imported_blueprint.suggested_filename == "balloob/test-topic" + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS + assert ( + imported_blueprint.suggested_filename + == "frenck/zha-ikea-five-button-remote-for-lights" + ) assert ( imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json index 5b9a3dcb9c..121d53ad94 100644 --- a/tests/fixtures/blueprint/community_post.json +++ b/tests/fixtures/blueprint/community_post.json @@ -2,39 +2,58 @@ "post_stream": { "posts": [ { - "id": 1144853, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:12.688Z", - "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e", + "id": 1216212, + "name": "Franck Nijhof", + "username": "frenck", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "created_at": "2020-12-10T09:20:58.974Z", + "cooked": "\u003cp\u003eThis is a blueprint for the IKEA five-button remotes (the round ones), specifically for use with ZHA.\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg\" data-download-href=\"/uploads/short-url/8SdGCUtkzOTNpMjggpBvSFs4WQ.jpeg?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg\" alt=\"image\" data-base62-sha1=\"8SdGCUtkzOTNpMjggpBvSFs4WQ\" width=\"500\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_750x750.jpeg 1.5x, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_1000x1000.jpeg 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e1400×1400 150 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eIt was specially created for use with (any) light(s). As the basic light controls are already mapped in this blueprint.\u003c/p\u003e\n\u003cp\u003eThe middle “on” button, toggle the lights on/off to the last set brightness (unless the force brightness is toggled on in the blueprint). Dim up/down buttons will change the brightness smoothly and can be pressed and hold until the brightness is satisfactory.\u003c/p\u003e\n\u003cp\u003eThe “left” and “right” buttons can be assigned to a short and long button press action. This allows you to assign, e.g., a scene or anything else.\u003c/p\u003e\n\u003cp\u003eThis is what the Blueprint looks like from the UI:\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png\" data-download-href=\"/uploads/short-url/mf5vhlKYe6yeuFayUzlCTBfveKf.png?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png\" alt=\"image\" data-base62-sha1=\"mf5vhlKYe6yeuFayUzlCTBfveKf\" width=\"610\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png, https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_915x750.png 1.5x, https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e975×799 64.1 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eBlueprint, which you can import by using this forum topic URL:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003eblueprint:\n name: ZHA - IKEA five button remote for lights\n description: |\n Control lights with an IKEA five button remote (the round ones).\n\n The middle \"on\" button, toggle the lights on/off to the last set brightness\n (unless the force brightness is toggled on in the blueprint).\n\n Dim up/down buttons will change the brightness smoothly and can be pressed\n and hold until the brightness is satisfactory.\n\n The \"left\" and \"right\" buttons can be assigned to a short and long button\n press action. This allows you to assign, e.g., a scene or anything else.\n\n domain: automation\n input:\n remote:\n name: Remote\n description: IKEA remote to use\n selector:\n device:\n integration: zha\n manufacturer: IKEA of Sweden\n model: TRADFRI remote control\n light:\n name: Light(s)\n description: The light(s) to control\n selector:\n target:\n entity:\n domain: light\n force_brightness:\n name: Force turn on brightness\n description: \u0026gt;\n Force the brightness to the set level below, when the \"on\" button on\n the remote is pushed and lights turn on.\n default: false\n selector:\n boolean:\n brightness:\n name: Brightness\n description: Brightness of the light(s) when turning on\n default: 50\n selector:\n number:\n min: 0\n max: 100\n mode: slider\n step: 1\n unit_of_measurement: \"%\"\n button_left_short:\n name: Left button - short press\n description: Action to run on short left button press\n default: []\n selector:\n action:\n button_left_long:\n name: Left button - long press\n description: Action to run on long left button press\n default: []\n selector:\n action:\n button_right_short:\n name: Right button - short press\n description: Action to run on short right button press\n default: []\n selector:\n action:\n button_right_long:\n name: Right button - long press\n description: Action to run on long right button press\n default: []\n selector:\n action:\n\nmode: restart\nmax_exceeded: silent\n\nvariables:\n force_brightness: !input force_brightness\n\ntrigger:\n - platform: event\n event_type: zha_event\n event_data:\n device_id: !input remote\n\naction:\n - variables:\n command: \"{{ trigger.event.data.command }}\"\n cluster_id: \"{{ trigger.event.data.cluster_id }}\"\n endpoint_id: \"{{ trigger.event.data.endpoint_id }}\"\n args: \"{{ trigger.event.data.args }}\"\n - choose:\n - conditions:\n - \"{{ command == 'toggle' }}\"\n - \"{{ cluster_id == 6 }}\"\n - \"{{ endpoint_id == 1 }}\"\n sequence:\n - choose:\n - conditions: \"{{ force_brightness }}\"\n sequence:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n brightness_pct: !input brightness\n default:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n\n - conditions:\n - \"{{ command == 'step_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'step' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [257, 13, 0] }}\"\n sequence: !input button_left_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3329, 0] }}\"\n sequence: !input button_left_long\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [256, 13, 0] }}\"\n sequence: !input button_right_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3328, 0] }}\"\n sequence: !input button_right_long\n\u003c/code\u003e\u003c/pre\u003e", "post_number": 1, "post_type": 1, - "updated_at": "2020-10-20T08:24:14.189Z", + "updated_at": "2020-12-10T09:22:08.993Z", "reply_count": 0, "reply_to_post_number": null, "quote_count": 0, "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", + "reads": 3, + "readers_count": 2, + "score": 0.6, + "yours": false, + "topic_id": 253804, + "topic_slug": "zha-ikea-five-button-remote-for-lights", + "display_username": "Franck Nijhof", "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_bg_color": null, "primary_group_flair_color": null, - "version": 2, + "version": 1, "can_edit": true, "can_delete": false, "can_recover": false, "can_wiki": true, + "link_counts": [ + { + "url": "https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "internal": false, + "reflection": false, + "title": "9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "clicks": 0 + }, + { + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "internal": false, + "reflection": false, + "title": "0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "clicks": 0 + } + ], "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, + "user_title": null, "actions_summary": [ + { + "id": 2, + "can_act": true + }, { "id": 3, "can_act": true @@ -48,75 +67,7 @@ "can_act": true }, { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144854, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:17.535Z", - "cooked": "", - "post_number": 2, - "post_type": 3, - "updated_at": "2020-10-16T12:20:17.535Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 1, - "reads": 2, - "readers_count": 1, - "score": 5.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, + "id": 6, "can_act": true }, { @@ -127,82 +78,9 @@ "moderator": true, "admin": true, "staff": true, - "user_id": 3, + "user_id": 10250, "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "action_code": "visible.disabled", - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144872, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:27:53.926Z", - "cooked": "\u003cp\u003eTest reply!\u003c/p\u003e", - "post_number": 3, - "post_type": 1, - "updated_at": "2020-10-16T12:27:53.926Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, - "can_act": true - }, - { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, + "trust_level": 4, "deleted_at": null, "user_deleted": false, "edit_reason": null, @@ -211,7 +89,7 @@ "reviewable_id": 0, "reviewable_score_count": 0, "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", + "user_created_at": "2017-08-12T12:46:55.467Z", "user_date_of_birth": null, "user_signature": null, "can_accept_answer": false, @@ -220,36 +98,34 @@ } ], "stream": [ - 1144853, - 1144854, - 1144872 + 1216212 ] }, "timeline_lookup": [ [ 1, - 3 + 0 ] ], "suggested_topics": [ { - "id": 17750, - "title": "Tutorial: Creating your first add-on", - "fancy_title": "Tutorial: Creating your first add-on", - "slug": "tutorial-creating-your-first-add-on", - "posts_count": 26, - "reply_count": 14, - "highest_post_number": 27, - "image_url": null, - "created_at": "2017-05-14T07:51:33.946Z", - "last_posted_at": "2020-07-28T11:29:27.892Z", + "id": 168593, + "title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "fancy_title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "slug": "dwains-dashboard-1-click-install-lovelace-dashboard-for-desktop-tablet-and-mobile-v2-0-0", + "posts_count": 1162, + "reply_count": 785, + "highest_post_number": 1185, + "image_url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg", + "created_at": "2020-02-03T13:15:24.364Z", + "last_posted_at": "2020-12-10T07:57:47.304Z", "bumped": true, - "bumped_at": "2020-07-28T11:29:27.892Z", + "bumped_at": "2020-12-10T07:57:47.304Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 18, - "unread": 7, - "new_posts": 2, + "last_read_post_number": 81, + "unread": 0, + "new_posts": 1109, "pinned": false, "unpinned": null, "visible": true, @@ -258,11 +134,19 @@ "notification_level": 2, "bookmarked": false, "liked": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 296, + "height": 50, + "url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg" + } + ], "tags": [], - "like_count": 9, - "views": 4355, - "category_id": 25, + "like_count": 1214, + "views": 71580, + "category_id": 34, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -270,50 +154,50 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 36674, + "username": "dwains", + "name": "Dwain Scheeren", + "avatar_template": "/user_avatar/community.home-assistant.io/dwains/{size}/100261_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9852, - "username": "JSCSJSCS", + "id": 16514, + "username": "jimpower", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/jscsjscs/{size}/38256_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/jimpower/{size}/66909_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 11494, - "username": "so3n", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/so3n/{size}/46007_2.png" + "id": 1473, + "username": "thundergreen", + "name": "Thundergreen", + "avatar_template": "/user_avatar/community.home-assistant.io/thundergreen/{size}/18379_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9094, - "username": "IoTnerd", - "name": "Balázs Suhajda", - "avatar_template": "/user_avatar/community.home-assistant.io/iotnerd/{size}/33526_2.png" + "id": 64369, + "username": "MRobi", + "name": "Mike", + "avatar_template": "/user_avatar/community.home-assistant.io/mrobi/{size}/113127_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 73134, - "username": "diord", - "name": "", - "avatar_template": "/letter_avatar/diord/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 9646, + "username": "Freshhat", + "name": "Freshhat", + "avatar_template": "/user_avatar/community.home-assistant.io/freshhat/{size}/24797_2.png" } } ] @@ -323,19 +207,19 @@ "title": "Lovelace: Button card", "fancy_title": "Lovelace: Button card", "slug": "lovelace-button-card", - "posts_count": 4608, - "reply_count": 3522, - "highest_post_number": 4691, + "posts_count": 4775, + "reply_count": 3635, + "highest_post_number": 4858, "image_url": null, "created_at": "2018-08-28T00:18:19.312Z", - "last_posted_at": "2020-10-20T07:33:29.523Z", + "last_posted_at": "2020-12-10T04:42:58.851Z", "bumped": true, - "bumped_at": "2020-10-20T07:33:29.523Z", + "bumped_at": "2020-12-10T04:42:58.851Z", "archetype": "regular", "unseen": false, "last_read_post_number": 1938, "unread": 369, - "new_posts": 2384, + "new_posts": 2551, "pinned": false, "unpinned": null, "visible": true, @@ -346,8 +230,8 @@ "liked": false, "thumbnails": null, "tags": [], - "like_count": 1700, - "views": 184752, + "like_count": 1740, + "views": 199965, "category_id": 34, "featured_link": null, "has_accepted_answer": false, @@ -366,20 +250,20 @@ "extras": null, "description": "Frequent Poster", "user": { - "id": 2019, - "username": "iantrich", - "name": "Ian", - "avatar_template": "/user_avatar/community.home-assistant.io/iantrich/{size}/154042_2.png" + "id": 33228, + "username": "jimz011", + "name": "Jim", + "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 33228, - "username": "jimz011", - "name": "Jim", - "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" + "id": 12475, + "username": "Mariusthvdb", + "name": "Marius", + "avatar_template": "/user_avatar/community.home-assistant.io/mariusthvdb/{size}/49008_2.png" } }, { @@ -396,32 +280,32 @@ "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 26227, - "username": "RomRider", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/romrider/{size}/41384_2.png" + "id": 52090, + "username": "parautenbach", + "name": "Pieter Rautenbach", + "avatar_template": "/user_avatar/community.home-assistant.io/parautenbach/{size}/89345_2.png" } } ] }, { - "id": 10564, - "title": "Professional/Commercial Use?", - "fancy_title": "Professional/Commercial Use?", - "slug": "professional-commercial-use", - "posts_count": 54, - "reply_count": 37, - "highest_post_number": 54, + "id": 58639, + "title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "fancy_title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "slug": "echo-devices-alexa-as-media-player-testers-needed", + "posts_count": 4429, + "reply_count": 3009, + "highest_post_number": 4517, "image_url": null, - "created_at": "2017-01-27T05:01:57.453Z", - "last_posted_at": "2020-10-20T07:03:57.895Z", + "created_at": "2018-07-04T03:36:22.187Z", + "last_posted_at": "2020-12-10T04:26:11.298Z", "bumped": true, - "bumped_at": "2020-10-20T07:03:57.895Z", + "bumped_at": "2020-12-10T04:26:11.298Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, + "last_read_post_number": 3219, "unread": 0, - "new_posts": 47, + "new_posts": 1298, "pinned": false, "unpinned": null, "visible": true, @@ -431,104 +315,12 @@ "bookmarked": false, "liked": false, "thumbnails": null, - "tags": [], - "like_count": 21, - "views": 10695, - "category_id": 17, - "featured_link": null, - "has_accepted_answer": false, - "posters": [ - { - "extras": null, - "description": "Original Poster", - "user": { - "id": 4758, - "username": "oobie11", - "name": "Bryan", - "avatar_template": "/user_avatar/community.home-assistant.io/oobie11/{size}/37858_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 18386, - "username": "pitp2", - "name": "", - "avatar_template": "/letter_avatar/pitp2/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 23116, - "username": "jortegamx", - "name": "Jake", - "avatar_template": "/user_avatar/community.home-assistant.io/jortegamx/{size}/45515_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 39038, - "username": "orif73", - "name": "orif73", - "avatar_template": "/letter_avatar/orif73/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": "latest", - "description": "Most Recent Poster", - "user": { - "id": 41040, - "username": "devastator", - "name": "", - "avatar_template": "/letter_avatar/devastator/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - } - ] - }, - { - "id": 219480, - "title": "What the heck is with the 'latest state change' not being kept after restart?", - "fancy_title": "What the heck is with the \u0026lsquo;latest state change\u0026rsquo; not being kept after restart?", - "slug": "what-the-heck-is-with-the-latest-state-change-not-being-kept-after-restart", - "posts_count": 37, - "reply_count": 13, - "highest_post_number": 38, - "image_url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png", - "created_at": "2020-08-18T13:10:09.367Z", - "last_posted_at": "2020-10-20T00:32:07.312Z", - "bumped": true, - "bumped_at": "2020-10-20T00:32:07.312Z", - "archetype": "regular", - "unseen": false, - "last_read_post_number": 8, - "unread": 0, - "new_posts": 30, - "pinned": false, - "unpinned": null, - "visible": true, - "closed": false, - "archived": false, - "notification_level": 2, - "bookmarked": false, - "liked": false, - "thumbnails": [ - { - "max_width": null, - "max_height": null, - "width": 469, - "height": 59, - "url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png" - } + "tags": [ + "alexa" ], - "tags": [], - "like_count": 26, - "views": 1722, - "category_id": 52, + "like_count": 1092, + "views": 179580, + "category_id": 47, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -536,72 +328,72 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3124, - "username": "andriej", + "id": 1084, + "username": "keatontaylor", + "name": "Keatontaylor", + "avatar_template": "/letter_avatar/keatontaylor/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 24884, + "username": "h4nc", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/andriej/{size}/24457_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/h4nc/{size}/68244_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 15052, - "username": "Misiu", + "id": 9191, + "username": "finity", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/misiu/{size}/20752_2.png" + "avatar_template": "/letter_avatar/finity/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 4629, - "username": "lolouk44", - "name": "lolouk44", - "avatar_template": "/user_avatar/community.home-assistant.io/lolouk44/{size}/119845_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 51736, - "username": "hmoffatt", - "name": "Hamish Moffatt", - "avatar_template": "/user_avatar/community.home-assistant.io/hmoffatt/{size}/88700_2.png" + "id": 1269, + "username": "ReneTode", + "name": "", + "avatar_template": "/user_avatar/community.home-assistant.io/renetode/{size}/1533_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 78711, - "username": "Astrosteve", - "name": "Steve", - "avatar_template": "/letter_avatar/astrosteve/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 46136, + "username": "chirad", + "name": "Dinoj", + "avatar_template": "/letter_avatar/chirad/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } } ] }, { - "id": 162594, - "title": "A different take on designing a Lovelace UI", - "fancy_title": "A different take on designing a Lovelace UI", - "slug": "a-different-take-on-designing-a-lovelace-ui", - "posts_count": 641, - "reply_count": 425, - "highest_post_number": 654, + "id": 252336, + "title": "Unhealthy state", + "fancy_title": "Unhealthy state", + "slug": "unhealthy-state", + "posts_count": 89, + "reply_count": 69, + "highest_post_number": 91, "image_url": null, - "created_at": "2020-01-11T23:09:25.207Z", - "last_posted_at": "2020-10-19T23:32:15.555Z", + "created_at": "2020-12-05T20:32:00.864Z", + "last_posted_at": "2020-12-09T22:41:30.212Z", "bumped": true, - "bumped_at": "2020-10-19T23:32:15.555Z", + "bumped_at": "2020-12-09T22:41:30.212Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, - "unread": 32, - "new_posts": 615, + "last_read_post_number": 75, + "unread": 0, + "new_posts": 16, "pinned": false, "unpinned": null, "visible": true, @@ -609,12 +401,12 @@ "archived": false, "notification_level": 2, "bookmarked": false, - "liked": false, + "liked": true, "thumbnails": null, "tags": [], - "like_count": 453, - "views": 68547, - "category_id": 9, + "like_count": 33, + "views": 946, + "category_id": 11, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -622,90 +414,179 @@ "extras": null, "description": "Original Poster", "user": { - "id": 11256, - "username": "Mattias_Persson", - "name": "Mattias Persson", - "avatar_template": "/user_avatar/community.home-assistant.io/mattias_persson/{size}/14773_2.png" + "id": 26121, + "username": "helgemor", + "name": "Helge", + "avatar_template": "/user_avatar/community.home-assistant.io/helgemor/{size}/42574_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 27634, - "username": "Jason_hill", - "name": "Jason Hill", - "avatar_template": "/user_avatar/community.home-assistant.io/jason_hill/{size}/93218_2.png" + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46782, - "username": "Martin_Pejstrup", - "name": "mpejstrup", - "avatar_template": "/user_avatar/community.home-assistant.io/martin_pejstrup/{size}/78412_2.png" + "id": 28146, + "username": "123", + "name": "Taras", + "avatar_template": "/user_avatar/community.home-assistant.io/123/{size}/44349_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46841, - "username": "spudje", - "name": "", - "avatar_template": "/letter_avatar/spudje/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 8361, + "username": "kanga_who", + "name": "Jason", + "avatar_template": "/user_avatar/community.home-assistant.io/kanga_who/{size}/46427_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 20924, - "username": "Diego_Santos", - "name": "Diego Santos", - "avatar_template": "/user_avatar/community.home-assistant.io/diego_santos/{size}/29096_2.png" + "id": 44704, + "username": "joselito1", + "name": "jose litomans", + "avatar_template": "/user_avatar/community.home-assistant.io/joselito1/{size}/75914_2.png" + } + } + ] + }, + { + "id": 130280, + "title": "Home Assistant Cast", + "fancy_title": "Home Assistant Cast", + "slug": "home-assistant-cast", + "posts_count": 282, + "reply_count": 206, + "highest_post_number": 289, + "image_url": null, + "created_at": "2019-08-06T15:59:00.183Z", + "last_posted_at": "2020-12-09T16:48:51.132Z", + "bumped": true, + "bumped_at": "2020-12-09T16:48:51.132Z", + "archetype": "regular", + "unseen": false, + "last_read_post_number": 88, + "unread": 0, + "new_posts": 201, + "pinned": false, + "unpinned": null, + "visible": true, + "closed": false, + "archived": false, + "notification_level": 3, + "bookmarked": false, + "liked": false, + "thumbnails": null, + "tags": [], + "like_count": 94, + "views": 29308, + "category_id": 30, + "featured_link": null, + "has_accepted_answer": false, + "posters": [ + { + "extras": null, + "description": "Original Poster", + "user": { + "id": -1, + "username": "system", + "name": "system", + "avatar_template": "/user_avatar/community.home-assistant.io/system/{size}/13_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 11649, + "username": "DavidFW1960", + "name": "David", + "avatar_template": "/user_avatar/community.home-assistant.io/davidfw1960/{size}/66886_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 26084, + "username": "Yoinkz", + "name": "", + "avatar_template": "/letter_avatar/yoinkz/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" + } + }, + { + "extras": "latest", + "description": "Most Recent Poster", + "user": { + "id": 45396, + "username": "Wetzel402", + "name": "Cody", + "avatar_template": "/user_avatar/community.home-assistant.io/wetzel402/{size}/76694_2.png" } } ] } ], - "tags": [], - "id": 236133, - "title": "Test Topic", - "fancy_title": "Test Topic", - "posts_count": 3, - "created_at": "2020-10-16T12:20:12.580Z", - "views": 13, + "tags": [ + "blueprint", + "zha" + ], + "id": 253804, + "title": "ZHA - IKEA five button remote for lights", + "fancy_title": "ZHA - IKEA five button remote for lights", + "posts_count": 1, + "created_at": "2020-12-10T09:20:58.681Z", + "views": 4, "reply_count": 0, "like_count": 0, - "last_posted_at": "2020-10-16T12:27:53.926Z", - "visible": false, + "last_posted_at": "2020-12-10T09:20:58.974Z", + "visible": true, "closed": false, "archived": false, "has_summary": false, "archetype": "regular", - "slug": "test-topic", - "category_id": 1, - "word_count": 37, + "slug": "zha-ikea-five-button-remote-for-lights", + "category_id": 53, + "word_count": 633, "deleted_at": null, - "user_id": 3, + "user_id": 10250, "featured_link": null, "pinned_globally": false, "pinned_at": null, "pinned_until": null, - "image_url": null, + "image_url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", "draft": null, - "draft_key": "topic_236133", - "draft_sequence": 8, - "posted": true, + "draft_key": "topic_253804", + "draft_sequence": 0, + "posted": false, "unpinned": null, "pinned": false, "current_post_number": 1, - "highest_post_number": 3, - "last_read_post_number": 3, - "last_read_post_id": 1144872, + "highest_post_number": 1, + "last_read_post_number": 1, + "last_read_post_id": 1216212, "deleted_by": null, "has_deleted": false, "actions_summary": [ @@ -732,16 +613,24 @@ "bookmarked": false, "topic_timer": null, "private_topic_timer": null, - "message_bus_last_id": 5, + "message_bus_last_id": 4, "participant_count": 1, "show_read_indicator": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 1400, + "height": 1400, + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg" + } + ], "can_vote": false, "vote_count": null, "user_voted": false, "details": { - "notification_level": 3, - "notifications_reason_id": 1, + "notification_level": 1, + "notifications_reason_id": null, "can_move_posts": true, "can_edit": true, "can_delete": true, @@ -756,11 +645,11 @@ "can_remove_self_id": 3, "participants": [ { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "post_count": 3, + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "post_count": 1, "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_color": null, @@ -768,16 +657,16 @@ } ], "created_by": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" }, "last_poster": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" } } } From 0da312b6f15730be69fe90f12d1be0faa70cc5ca Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Dec 2020 16:45:57 +0100 Subject: [PATCH 081/302] Fix importing blueprint from community (#44104) --- homeassistant/components/blueprint/importer.py | 4 ++-- tests/components/blueprint/test_importer.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index f0230aba1b..217851df98 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -108,10 +108,10 @@ def _extract_blueprint_from_community_topic( if block_syntax not in ("auto", "yaml"): continue - block_content = block_content.strip() + block_content = html.unescape(block_content.strip()) try: - data = yaml.parse_yaml(html.unescape(block_content)) + data = yaml.parse_yaml(block_content) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 8e674e3a9d..382363aa56 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -173,6 +173,7 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" ) + assert "gt;" not in imported_blueprint.raw_data @pytest.mark.parametrize( From e306308a472ccf67fdd60a832e1f2f89b16d0af1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Dec 2020 17:54:55 +0100 Subject: [PATCH 082/302] Update frontend to 20201210.0 (#44105) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 65fe745e51..cb52afd4b6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201204.0"], + "requirements": ["home-assistant-frontend==20201210.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5e4bb07394..7e69ae0c9e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 57dcee73b7..c8b5ded4cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 262b0e816e..d5a21ee3a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 6253054fd4d740ccdddfe5537d0b0da82686d796 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 10 Dec 2020 14:41:31 -0500 Subject: [PATCH 083/302] Bump up dependencies on pyserial and pyserial-asyncio (#44089) --- homeassistant/components/acer_projector/manifest.json | 2 +- homeassistant/components/serial/manifest.json | 2 +- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/rfxtrx/test_config_flow.py | 2 +- tests/components/zha/test_config_flow.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 861e483adb..096d2c6e24 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -2,6 +2,6 @@ "domain": "acer_projector", "name": "Acer Projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector", - "requirements": ["pyserial==3.4"], + "requirements": ["pyserial==3.5"], "codeowners": [] } diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index d8305d1055..ce85d07d08 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -2,6 +2,6 @@ "domain": "serial", "name": "Serial", "documentation": "https://www.home-assistant.io/integrations/serial", - "requirements": ["pyserial-asyncio==0.4"], + "requirements": ["pyserial-asyncio==0.5"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f1821c9e48..7a36348b72 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,8 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows==0.21.0", - "pyserial==3.4", - "pyserial-asyncio==0.4", + "pyserial==3.5", + "pyserial-asyncio==0.5", "zha-quirks==0.0.48", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", diff --git a/requirements_all.txt b/requirements_all.txt index c8b5ded4cc..65bd29256a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,11 +1662,11 @@ pysensibo==1.0.3 # homeassistant.components.serial # homeassistant.components.zha -pyserial-asyncio==0.4 +pyserial-asyncio==0.5 # homeassistant.components.acer_projector # homeassistant.components.zha -pyserial==3.4 +pyserial==3.5 # homeassistant.components.sesame pysesame2==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5a21ee3a1..8497227b16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -836,11 +836,11 @@ pyruckus==0.12 # homeassistant.components.serial # homeassistant.components.zha -pyserial-asyncio==0.4 +pyserial-asyncio==0.5 # homeassistant.components.acer_projector # homeassistant.components.zha -pyserial==3.4 +pyserial==3.5 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 6ba045d60a..04545a1a42 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -29,7 +29,7 @@ def serial_connect_fail(self): def com_port(): """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo() + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = "/dev/ttyUSB1234" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 709b9a0ff2..6fcc369182 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry def com_port(): """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo() + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = "/dev/ttyUSB1234" From 9651d1bcfa65c0da59ff29b039d41ebddc5de7bb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Dec 2020 21:25:50 +0100 Subject: [PATCH 084/302] Support more errors to better do retries in UniFi (#44108) --- homeassistant/components/unifi/controller.py | 14 ++++++++++++-- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 16 ++++++++++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index a4435ccfec..30b82c65c8 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -397,7 +397,12 @@ class UniFiController: await self.api.login() self.api.start_websocket() - except (asyncio.TimeoutError, aiounifi.AiounifiException): + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.AiounifiException, + ): self.hass.loop.call_later(RETRY_TIMER, self.reconnect) @callback @@ -464,7 +469,12 @@ async def get_controller( LOGGER.warning("Connected to UniFi at %s but not registered.", host) raise AuthenticationRequired from err - except (asyncio.TimeoutError, aiounifi.RequestError) as err: + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.RequestError, + ) as err: LOGGER.error("Error connecting to the UniFi controller at %s", host) raise CannotConnect from err diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 48c080f82f..94b1c90f4f 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==25"], + "requirements": ["aiounifi==26"], "codeowners": ["@Kane610"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 65bd29256a..79f1472043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8497227b16..9604d0579a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 83732601cd..8d5cb85bf9 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -295,6 +295,22 @@ async def test_get_controller_login_failed(hass): await get_controller(hass, **CONTROLLER_DATA) +async def test_get_controller_controller_bad_gateway(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.BadGateway + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + +async def test_get_controller_controller_service_unavailable(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.ServiceUnavailable + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( From 97edbaa85f97384ca47b5029092f1fee00c8e164 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Dec 2020 21:30:07 +0100 Subject: [PATCH 085/302] Small cleanup of MQTT (#44110) * Use relative imports of mqtt component in mqtt platforms * Correct parameters to _async_setup_entity * Lint --- .../components/mqtt/alarm_control_panel.py | 2 +- .../components/mqtt/binary_sensor.py | 3 ++- homeassistant/components/mqtt/camera.py | 3 ++- homeassistant/components/mqtt/climate.py | 3 ++- homeassistant/components/mqtt/cover.py | 3 ++- .../components/mqtt/device_automation.py | 2 +- .../mqtt/device_tracker/schema_discovery.py | 3 ++- .../mqtt/device_tracker/schema_yaml.py | 2 +- .../components/mqtt/device_trigger.py | 2 +- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/fan.py | 3 ++- .../components/mqtt/light/__init__.py | 8 ++----- .../components/mqtt/light/schema_basic.py | 24 +++++++++---------- .../components/mqtt/light/schema_json.py | 24 +++++++++---------- .../components/mqtt/light/schema_template.py | 24 +++++++++---------- homeassistant/components/mqtt/lock.py | 3 ++- homeassistant/components/mqtt/scene.py | 3 ++- homeassistant/components/mqtt/sensor.py | 3 ++- homeassistant/components/mqtt/subscription.py | 2 +- homeassistant/components/mqtt/switch.py | 5 ++-- homeassistant/components/mqtt/tag.py | 2 +- homeassistant/components/mqtt/trigger.py | 3 ++- .../components/mqtt/vacuum/__init__.py | 12 ++++------ .../components/mqtt/vacuum/schema_legacy.py | 16 ++++++------- .../components/mqtt/vacuum/schema_state.py | 24 +++++++++---------- 25 files changed, 92 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 35d0e1fb42..edf383a681 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -4,7 +4,6 @@ import re import voluptuous as vol -from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -48,6 +47,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 339b41a9dd..e081423d59 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components import binary_sensor, mqtt +from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, BinarySensorEntity, @@ -40,6 +40,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 82e5cb8b27..e8783f74bd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import camera, mqtt +from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback @@ -23,6 +23,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 8b762a82f0..c5835f8e7c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import climate, mqtt +from homeassistant.components import climate from homeassistant.components.climate import ( PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, @@ -64,6 +64,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index c3a7813324..25fcf0ad0d 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import cover, mqtt +from homeassistant.components import cover from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -51,6 +51,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 4fcfd8f66f..c064cca599 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ATTR_DISCOVERY_HASH, device_trigger +from .. import mqtt from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index aa45f8f92b..4de2ae4fa6 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import device_tracker, mqtt +from homeassistant.components import device_tracker from homeassistant.components.device_tracker import SOURCE_TYPES from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( @@ -29,6 +29,7 @@ from .. import ( MqttEntityDeviceInfo, subscription, ) +from ... import mqtt from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index 520bced238..f871ac89c2 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -2,12 +2,12 @@ import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from ... import mqtt from ..const import CONF_QOS CONF_PAYLOAD_HOME = "payload_home" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 676252c313..9fa51bebf0 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -5,7 +5,6 @@ from typing import Callable, List, Optional import attr import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE @@ -28,6 +27,7 @@ from . import ( debug_info, trigger as mqtt_trigger, ) +from .. import mqtt from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index ca576f83d2..5452d15aa3 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -6,12 +6,12 @@ import logging import re import time -from homeassistant.components import mqtt from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt +from .. import mqtt from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS from .const import ( ATTR_DISCOVERY_HASH, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 14469e415e..96d5fe720c 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import fan, mqtt +from homeassistant.components import fan from homeassistant.components.fan import ( ATTR_SPEED, SPEED_HIGH, @@ -43,6 +43,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 2375fb86e5..393cb2fcf1 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -4,16 +4,12 @@ import logging import voluptuous as vol from homeassistant.components import light -from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH -from homeassistant.components.mqtt.discovery import ( - MQTT_DISCOVERY_NEW, - clear_discovery_hash, -) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .. import DOMAIN, PLATFORMS +from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS +from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 4796652f57..00ad267139 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -17,17 +16,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -43,6 +31,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index bba5605348..bb10fd52ae 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -4,7 +4,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -24,17 +23,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, @@ -54,6 +42,18 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index faf987881b..e6b22da5af 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -21,17 +20,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -45,6 +33,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index aea1e40b0f..712f2e0e37 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import lock, mqtt +from homeassistant.components import lock from homeassistant.components.lock import LockEntity from homeassistant.const import ( CONF_DEVICE, @@ -32,6 +32,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 4f4380332f..673eb169b1 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt, scene +from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID import homeassistant.helpers.config_validation as cv @@ -21,6 +21,7 @@ from . import ( MqttAvailability, MqttDiscoveryUpdate, ) +from .. import mqtt from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index ffd34cef8c..1fda8986ef 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -5,7 +5,7 @@ from typing import Optional import voluptuous as vol -from homeassistant.components import mqtt, sensor +from homeassistant.components import sensor from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_DEVICE, @@ -38,6 +38,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 24c1c6ff3a..c61c30c922 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -4,11 +4,11 @@ from typing import Any, Callable, Dict, Optional import attr -from homeassistant.components import mqtt from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from . import debug_info +from .. import mqtt from .const import DEFAULT_QOS from .models import MessageCallbackType diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 761f19ef05..7601968011 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt, switch +from homeassistant.components import switch from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( CONF_DEVICE, @@ -37,6 +37,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash @@ -73,7 +74,7 @@ async def async_setup_platform( ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities, discovery_info) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 94356ccf77..75f3bb5030 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -21,6 +20,7 @@ from . import ( cleanup_device_registry, subscription, ) +from .. import mqtt from .discovery import MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash from .util import valid_subscribe_topic diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 58ec51c4b1..1c96b3de26 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -3,11 +3,12 @@ import json import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM from homeassistant.core import HassJob, callback import homeassistant.helpers.config_validation as cv +from .. import mqtt + # mypy: allow-untyped-defs CONF_ENCODING = "encoding" diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index b954a97e8f..f6265d1b96 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -3,16 +3,12 @@ import logging import voluptuous as vol -from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH -from homeassistant.components.mqtt.discovery import ( - MQTT_DISCOVERY_NEW, - clear_discovery_hash, -) from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service -from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS +from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS +from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -34,7 +30,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities, discovery_info) + await _async_setup_entity(config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -58,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry, discovery_data=None + config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT vacuum.""" setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 907e2e4a08..65acc9afc7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -4,14 +4,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt -from homeassistant.components.mqtt import ( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -36,6 +28,14 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level +from .. import ( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 9f75f38f1b..5a8666e5a2 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -4,18 +4,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, @@ -44,6 +32,18 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services From 9cc406fef90eff82da8bb84b918b99b9bf099aea Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Thu, 10 Dec 2020 21:50:51 +0100 Subject: [PATCH 086/302] Remove deprecated CONF_ALLOW_UNLOCK, CONF_API_KEY from Google Assistant (#44087) * Remove deprecated CONF_ALLOW_UNLOCK, CONF_API_KEY * Use vol.Remove() to prevent setup fail * Keep constants --- .../components/google_assistant/__init__.py | 14 +++++----- .../components/google_assistant/const.py | 2 -- .../components/google_assistant/http.py | 26 +------------------ 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 0afd66c10a..8f4ee3b51c 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -11,8 +11,6 @@ from homeassistant.helpers import config_validation as cv from .const import ( CONF_ALIASES, - CONF_ALLOW_UNLOCK, - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -36,6 +34,9 @@ from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, is _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_UNLOCK = "allow_unlock" +CONF_API_KEY = "api_key" + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, @@ -61,8 +62,6 @@ def _check_report_state(data): GOOGLE_ASSISTANT_SCHEMA = vol.All( - cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.105"), vol.Schema( { vol.Required(CONF_PROJECT_ID): cv.string, @@ -72,13 +71,14 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( vol.Optional( CONF_EXPOSED_DOMAINS, default=DEFAULT_EXPOSED_DOMAINS ): cv.ensure_list, - vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, - vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, # str on purpose, makes sure it is configured correctly. vol.Optional(CONF_SECURE_DEVICES_PIN): str, vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean, vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT, + # deprecated configuration options + vol.Remove(CONF_ALLOW_UNLOCK): cv.boolean, + vol.Remove(CONF_API_KEY): cv.string, }, extra=vol.PREVENT_EXTRA, ), @@ -113,7 +113,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): await google_config.async_sync_entities(agent_user_id) # Register service only if key is provided - if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config: + if CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 47ceabb20e..d6badf2e7b 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -30,9 +30,7 @@ CONF_EXPOSE_BY_DEFAULT = "expose_by_default" CONF_EXPOSED_DOMAINS = "exposed_domains" CONF_PROJECT_ID = "project_id" CONF_ALIASES = "aliases" -CONF_API_KEY = "api_key" CONF_ROOM_HINT = "room" -CONF_ALLOW_UNLOCK = "allow_unlock" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" CONF_REPORT_STATE = "report_state" CONF_SERVICE_ACCOUNT = "service_account" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 4bf0df8b93..5cf1cb1437 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -19,7 +19,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from .const import ( - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -135,11 +134,7 @@ class GoogleConfig(AbstractConfig): return True async def _async_request_sync_devices(self, agent_user_id: str): - if CONF_API_KEY in self._config: - await self.async_call_homegraph_api_key( - REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} - ) - elif CONF_SERVICE_ACCOUNT in self._config: + if CONF_SERVICE_ACCOUNT in self._config: await self.async_call_homegraph_api( REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} ) @@ -164,25 +159,6 @@ class GoogleConfig(AbstractConfig): self._access_token = token["access_token"] self._access_token_renew = now + timedelta(seconds=token["expires_in"]) - async def async_call_homegraph_api_key(self, url, data): - """Call a homegraph api with api key authentication.""" - websession = async_get_clientsession(self.hass) - try: - res = await websession.post( - url, params={"key": self._config.get(CONF_API_KEY)}, json=data - ) - _LOGGER.debug( - "Response on %s with data %s was %s", url, data, await res.text() - ) - res.raise_for_status() - return res.status - except ClientResponseError as error: - _LOGGER.error("Request for %s failed: %d", url, error.status) - return error.status - except (asyncio.TimeoutError, ClientError): - _LOGGER.error("Could not contact %s", url) - return HTTP_INTERNAL_SERVER_ERROR - async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authentication.""" session = async_get_clientsession(self.hass) From 7084d6c650d27babfabf0cf34a9e1aa492927820 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Dec 2020 22:17:58 +0100 Subject: [PATCH 087/302] Address old review comments of Tasmota fan (#44112) * Address review comments of Tasmota fan * Address review comment --- homeassistant/components/tasmota/fan.py | 4 +++- tests/components/tasmota/test_fan.py | 28 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 362149e9fc..bdcc00dc76 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -63,7 +63,7 @@ class TasmotaFan( @property def speed_list(self): """Get the list of available speeds.""" - return list(HA_TO_TASMOTA_SPEED_MAP.keys()) + return list(HA_TO_TASMOTA_SPEED_MAP) @property def supported_features(self): @@ -72,6 +72,8 @@ class TasmotaFan( async def async_set_speed(self, speed): """Set the speed of the fan.""" + if speed not in HA_TO_TASMOTA_SPEED_MAP: + raise ValueError(f"Unsupported speed {speed}") if speed == fan.SPEED_OFF: await self.async_turn_off() else: diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 5cadc20218..1aca8c84e0 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -7,6 +7,7 @@ from hatasmota.utils import ( get_topic_tele_state, get_topic_tele_will, ) +import pytest from homeassistant.components import fan from homeassistant.components.tasmota.const import DEFAULT_PREFIX @@ -152,6 +153,33 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): ) +async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Set an unsupported speed and verify MQTT message is not sent + with pytest.raises(ValueError) as excinfo: + await common.async_set_speed(hass, "fan.tasmota", "no_such_speed") + assert "Unsupported speed no_such_speed" in str(excinfo.value) + mqtt_mock.async_publish.assert_not_called() + + async def test_availability_when_connection_lost( hass, mqtt_client_mock, mqtt_mock, setup_tasmota ): From b4afef139500758f808656ceab26c5a6df7fc367 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 10 Dec 2020 13:24:26 -0800 Subject: [PATCH 088/302] Add tests for the wemo component (#44088) * Add tests for the wemo component. * Prefer mock tools from tests.async_mock over importing asynctest directly * Avoid using `entity/entities` except when referring to an `Entity` instance in wemo tests * Remove the overridden event_loop fixture from the wemo tests * Patch the library code, not the integration code, in the wemo tests --- .coveragerc | 1 - requirements_test_all.txt | 3 + tests/components/wemo/__init__.py | 1 + tests/components/wemo/conftest.py | 75 ++++++++++++++++++ tests/components/wemo/test_binary_sensor.py | 60 +++++++++++++++ tests/components/wemo/test_fan.py | 85 +++++++++++++++++++++ tests/components/wemo/test_light_bridge.py | 56 ++++++++++++++ tests/components/wemo/test_light_dimmer.py | 58 ++++++++++++++ tests/components/wemo/test_switch.py | 58 ++++++++++++++ 9 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 tests/components/wemo/__init__.py create mode 100644 tests/components/wemo/conftest.py create mode 100644 tests/components/wemo/test_binary_sensor.py create mode 100644 tests/components/wemo/test_fan.py create mode 100644 tests/components/wemo/test_light_bridge.py create mode 100644 tests/components/wemo/test_light_dimmer.py create mode 100644 tests/components/wemo/test_switch.py diff --git a/.coveragerc b/.coveragerc index c887fc385e..b267f6967e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1014,7 +1014,6 @@ omit = homeassistant/components/watson_tts/tts.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* - homeassistant/components/wemo/* homeassistant/components/whois/sensor.py homeassistant/components/wiffi/* homeassistant/components/wink/* diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9604d0579a..75f8f1cdec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -932,6 +932,9 @@ pyvolumio==0.1.3 # homeassistant.components.html5 pywebpush==1.9.2 +# homeassistant.components.wemo +pywemo==0.5.3 + # homeassistant.components.wilight pywilight==0.0.65 diff --git a/tests/components/wemo/__init__.py b/tests/components/wemo/__init__.py new file mode 100644 index 0000000000..33bdcacd37 --- /dev/null +++ b/tests/components/wemo/__init__.py @@ -0,0 +1 @@ +"""Tests for the wemo component.""" diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py new file mode 100644 index 0000000000..78262ccc59 --- /dev/null +++ b/tests/components/wemo/conftest.py @@ -0,0 +1,75 @@ +"""Fixtures for pywemo.""" +import pytest +import pywemo + +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.async_mock import create_autospec, patch + +MOCK_HOST = "127.0.0.1" +MOCK_PORT = 50000 +MOCK_NAME = "WemoDeviceName" +MOCK_SERIAL_NUMBER = "WemoSerialNumber" + + +@pytest.fixture(name="pywemo_model") +def pywemo_model_fixture(): + """Fixture containing a pywemo class name used by pywemo_device_fixture.""" + return "Insight" + + +@pytest.fixture(name="pywemo_registry") +def pywemo_registry_fixture(): + """Fixture for SubscriptionRegistry instances.""" + registry = create_autospec(pywemo.SubscriptionRegistry) + + registry.callbacks = {} + + def on_func(device, type_filter, callback): + registry.callbacks[device.name] = callback + + registry.on.side_effect = on_func + + with patch("pywemo.SubscriptionRegistry", return_value=registry): + yield registry + + +@pytest.fixture(name="pywemo_device") +def pywemo_device_fixture(pywemo_registry, pywemo_model): + """Fixture for WeMoDevice instances.""" + device = create_autospec(getattr(pywemo, pywemo_model)) + device.host = MOCK_HOST + device.port = MOCK_PORT + device.name = MOCK_NAME + device.serialnumber = MOCK_SERIAL_NUMBER + device.model_name = pywemo_model + + url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" + with patch("pywemo.setup_url_for_address", return_value=url), patch( + "pywemo.discovery.device_from_description", return_value=device + ): + yield device + + +@pytest.fixture(name="wemo_entity") +async def async_wemo_entity_fixture(hass, pywemo_device): + """Fixture for a Wemo entity in hass.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [f"{MOCK_HOST}:{MOCK_PORT}"], + }, + }, + ) + await hass.async_block_till_done() + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_registry.entities.values()) + assert len(entity_entries) == 1 + + yield entity_entries[0] diff --git a/tests/components/wemo/test_binary_sensor.py b/tests/components/wemo/test_binary_sensor.py new file mode 100644 index 0000000000..f217d0b168 --- /dev/null +++ b/tests/components/wemo/test_binary_sensor.py @@ -0,0 +1,60 @@ +"""Tests for the Wemo binary_sensor entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def pywemo_model(): + """Pywemo Motion models use the binary_sensor platform.""" + return "Motion" + + +async def test_binary_sensor_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the binary_sensor receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_binary_sensor_update_entity( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the binary_sensor performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py new file mode 100644 index 0000000000..ed49519c77 --- /dev/null +++ b/tests/components/wemo/test_fan.py @@ -0,0 +1,85 @@ +"""Tests for the Wemo fan entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.wemo import fan +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def pywemo_model(): + """Pywemo Humidifier models use the fan platform.""" + return "Humidifier" + + +async def test_fan_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the fan receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_fan_update_entity(hass, pywemo_registry, pywemo_device, wemo_entity): + """Verify that the fan performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity): + """Verify that SERVICE_RESET_FILTER_LIFE is registered and works.""" + assert await hass.services.async_call( + DOMAIN, + fan.SERVICE_RESET_FILTER_LIFE, + {fan.ATTR_ENTITY_ID: wemo_entity.entity_id}, + blocking=True, + ) + pywemo_device.reset_filter_life.assert_called_with() + + +async def test_fan_set_humidity_service(hass, pywemo_device, wemo_entity): + """Verify that SERVICE_SET_HUMIDITY is registered and works.""" + assert await hass.services.async_call( + DOMAIN, + fan.SERVICE_SET_HUMIDITY, + { + fan.ATTR_ENTITY_ID: wemo_entity.entity_id, + fan.ATTR_TARGET_HUMIDITY: "50", + }, + blocking=True, + ) + pywemo_device.set_humidity.assert_called_with(fan.WEMO_HUMIDITY_50) diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py new file mode 100644 index 0000000000..d76c714ba5 --- /dev/null +++ b/tests/components/wemo/test_light_bridge.py @@ -0,0 +1,56 @@ +"""Tests for the Wemo light entity via the bridge.""" + +import pytest +import pywemo + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from tests.async_mock import PropertyMock, create_autospec + + +@pytest.fixture +def pywemo_model(): + """Pywemo Bridge models use the light platform (WemoLight class).""" + return "Bridge" + + +@pytest.fixture(name="pywemo_bridge_light") +def pywemo_bridge_light_fixture(pywemo_device): + """Fixture for Bridge.Light WeMoDevice instances.""" + light = create_autospec(pywemo.ouimeaux_device.bridge.Light) + light.uniqueID = pywemo_device.serialnumber + light.name = pywemo_device.name + pywemo_device.Lights = {pywemo_device.serialnumber: light} + return light + + +async def test_light_update_entity( + hass, pywemo_registry, pywemo_bridge_light, wemo_entity +): + """Verify that the light performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + type(pywemo_bridge_light).state = PropertyMock(return_value={"onoff": 1}) + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + type(pywemo_bridge_light).state = PropertyMock(return_value={"onoff": 0}) + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_light_dimmer.py b/tests/components/wemo/test_light_dimmer.py new file mode 100644 index 0000000000..e94634c6d1 --- /dev/null +++ b/tests/components/wemo/test_light_dimmer.py @@ -0,0 +1,58 @@ +"""Tests for the Wemo standalone/non-bridge light entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def pywemo_model(): + """Pywemo Dimmer models use the light platform (WemoDimmer class).""" + return "Dimmer" + + +async def test_light_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the light receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_light_update_entity(hass, pywemo_registry, pywemo_device, wemo_entity): + """Verify that the light performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py new file mode 100644 index 0000000000..1ae8e3f945 --- /dev/null +++ b/tests/components/wemo/test_switch.py @@ -0,0 +1,58 @@ +"""Tests for the Wemo switch entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def pywemo_model(): + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitch" + + +async def test_switch_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the switch receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_switch_update_entity(hass, pywemo_registry, pywemo_device, wemo_entity): + """Verify that the switch performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF From 2bbe8e0e6ba363b8055796c8099e7ee8957964aa Mon Sep 17 00:00:00 2001 From: Huon Wilson Date: Fri, 11 Dec 2020 09:31:54 +1100 Subject: [PATCH 089/302] Cache Astral object in moon integration, to use less CPU (#44012) Creating an Astral() instance seems to be surprisingly expensive. This was previously being done in every update, taking a non-trivial proportion of the (actual) CPU used by HA, that is, ignoring sleep/wait/idle states like being blocked on epoll. This patch caches the Astral instance to avoid recomputing it regularly. --- homeassistant/components/moon/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index f7914177f8..9e0f8ef51d 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -49,6 +49,7 @@ class MoonSensor(Entity): """Initialize the moon sensor.""" self._name = name self._state = None + self._astral = Astral() @property def name(self): @@ -87,4 +88,4 @@ class MoonSensor(Entity): async def async_update(self): """Get the time and updates the states.""" today = dt_util.as_local(dt_util.utcnow()).date() - self._state = Astral().moon_phase(today) + self._state = self._astral.moon_phase(today) From a29f3e7163e1ed7c4d635a7adb2ea2f090530a59 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 11 Dec 2020 00:04:19 +0000 Subject: [PATCH 090/302] [ci skip] Translation update --- homeassistant/components/cover/translations/nl.json | 2 +- homeassistant/components/dsmr/translations/nl.json | 10 ++++++++++ homeassistant/components/gios/translations/it.json | 5 +++++ homeassistant/components/hassio/translations/ca.json | 2 +- .../components/homeassistant/translations/ca.json | 2 +- .../components/homeassistant/translations/nl.json | 2 +- homeassistant/components/tuya/translations/it.json | 3 +++ homeassistant/components/tuya/translations/tr.json | 3 +++ 8 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index 7d68d78641..679d9360a8 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -28,7 +28,7 @@ "state": { "_": { "closed": "Gesloten", - "closing": "Sluit", + "closing": "Sluiten", "open": "Open", "opening": "Opent", "stopped": "Gestopt" diff --git a/homeassistant/components/dsmr/translations/nl.json b/homeassistant/components/dsmr/translations/nl.json index 41edcd176d..ba31fa36fd 100644 --- a/homeassistant/components/dsmr/translations/nl.json +++ b/homeassistant/components/dsmr/translations/nl.json @@ -11,5 +11,15 @@ "one": "Leeg", "other": "Leeg" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minimumtijd tussen entiteitsupdates [s]" + }, + "title": "DSMR-opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index f1d7d60315..26bf8386d6 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index ac804794b4..0cdb931842 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -3,7 +3,7 @@ "info": { "board": "Placa", "disk_total": "Total disc", - "disk_used": "Disc utilitzat", + "disk_used": "Emmagatzematge utilitzat", "docker_version": "Versi\u00f3 de Docker", "healthy": "Saludable", "host_os": "Sistema operatiu amfitri\u00f3", diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index 8f9931c682..97e3d088af 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -3,7 +3,7 @@ "info": { "arch": "Arquitectura de la CPU", "chassis": "Xass\u00eds", - "dev": "Desenvolupament", + "dev": "Desenvolupador", "docker": "Docker", "docker_version": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index a4f73279a7..277c044c53 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -1,7 +1,7 @@ { "system_health": { "info": { - "dev": "Ontwikkelaarsmodus", + "dev": "Ontwikkeling", "docker": "Docker", "docker_version": "Docker", "os_version": "Versie van het besturingssysteem", diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 1277d8fca8..639f583492 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Impossibile connettersi" + }, "error": { "dev_multi_type": "Pi\u00f9 dispositivi selezionati da configurare devono essere dello stesso tipo", "dev_not_config": "Tipo di dispositivo non configurabile", diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index d2af1633f9..0dd8a3d554 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -1,5 +1,8 @@ { "options": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "init": { "data": { From ce3f6c368a023251b6ceb2aa866683bc1450912d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 11 Dec 2020 05:11:51 +0100 Subject: [PATCH 091/302] Initialize numeric_state trigger tests (#44114) This makes the crossed thresholds explicit. --- .../triggers/test_numeric_state.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index c8a9cd6d50..326990e12c 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -61,6 +61,9 @@ async def test_if_not_fires_on_entity_removal(hass, calls): async def test_if_fires_on_entity_change_below(hass, calls): """Test the firing with changed entity.""" + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + context = Context() assert await async_setup_component( hass, @@ -270,6 +273,9 @@ async def test_if_fires_on_initial_entity_above(hass, calls): async def test_if_fires_on_entity_change_above(hass, calls): """Test the firing with changed entity.""" + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -378,6 +384,9 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): async def test_if_fires_on_entity_change_below_range(hass, calls): """Test the firing with changed entity.""" + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -500,6 +509,9 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): """Test attributes change.""" + hass.states.async_set("test.entity", 11, {"test_attribute": 11}) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -544,6 +556,9 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): """Test attributes change.""" + hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -636,6 +651,10 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, calls): """Test attributes change.""" + hass.states.async_set( + "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 11} + ) + await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, @@ -661,6 +680,8 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, async def test_template_list(hass, calls): """Test template list.""" + hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) + await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, @@ -791,6 +812,9 @@ async def test_if_action(hass, calls): async def test_if_fails_setup_bad_for(hass, calls): """Test for setup failure for bad for.""" + hass.states.async_set("test.entity", 5) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -863,6 +887,10 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): """Test for not firing on entities change with for after stop.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -906,6 +934,9 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): """Test for firing on entity change with for and attribute change.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -941,6 +972,9 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): async def test_if_fires_on_entity_change_with_for(hass, calls): """Test for firing on entity change with for.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -967,6 +1001,9 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): async def test_wait_template_with_trigger(hass, calls): """Test using wait template with 'trigger.entity_id'.""" + hass.states.async_set("test.entity", "0") + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1004,6 +1041,10 @@ async def test_wait_template_with_trigger(hass, calls): async def test_if_fires_on_entities_change_no_overlap(hass, calls): """Test for firing on entities change with no overlap.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1047,6 +1088,10 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): async def test_if_fires_on_entities_change_overlap(hass, calls): """Test for firing on entities change with overlap.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1101,6 +1146,9 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): async def test_if_fires_on_change_with_for_template_1(hass, calls): """Test for firing on change with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1128,6 +1176,9 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): async def test_if_fires_on_change_with_for_template_2(hass, calls): """Test for firing on change with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1155,6 +1206,9 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): async def test_if_fires_on_change_with_for_template_3(hass, calls): """Test for firing on change with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1182,6 +1236,9 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): async def test_invalid_for_template(hass, calls): """Test for invalid for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1207,6 +1264,10 @@ async def test_invalid_for_template(hass, calls): async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): """Test for firing on entities change with overlap and for template.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, From e33405a8b06f25a63f92248c15bf91a1f17c1853 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Fri, 11 Dec 2020 10:53:21 -0500 Subject: [PATCH 092/302] Fix Met.no forecast precipitation (#44106) --- homeassistant/components/met/weather.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 73b9134415..c0c8c11c64 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -21,8 +21,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS, @@ -32,7 +34,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure -from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP +from .const import ( + ATTR_FORECAST_PRECIPITATION, + ATTR_MAP, + CONDITIONS_MAP, + CONF_TRACK_HOME, + DOMAIN, + FORECAST_MAP, +) _LOGGER = logging.getLogger(__name__) @@ -221,6 +230,14 @@ class MetWeather(CoordinatorEntity, WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } + if not self._is_metric: + if ATTR_FORECAST_PRECIPITATION in ha_item: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) + ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From e67809713f6a7273f0f5513f7b03f60d31e5afe6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Dec 2020 18:02:23 +0100 Subject: [PATCH 093/302] Nuki to use entity platform (#43774) --- homeassistant/components/nuki/lock.py | 81 ++++++++++++--------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index d8585ad745..d0b55514a6 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -8,11 +8,8 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.service import extract_entity_ids - -from . import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.helpers import config_validation as cv, entity_platform _LOGGER = logging.getLogger(__name__) @@ -28,8 +25,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) NUKI_DATA = "nuki" -SERVICE_LOCK_N_GO = "lock_n_go" - ERROR_STATES = (0, 254, 255) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -40,47 +35,38 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_UNLATCH, default=False): cv.boolean, - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Nuki lock platform.""" - bridge = NukiBridge( - config[CONF_HOST], - config[CONF_TOKEN], - config[CONF_PORT], - True, - DEFAULT_TIMEOUT, + + def get_entities(): + bridge = NukiBridge( + config[CONF_HOST], + config[CONF_TOKEN], + config[CONF_PORT], + True, + DEFAULT_TIMEOUT, + ) + + entities = [NukiLockEntity(lock) for lock in bridge.locks] + entities.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) + return entities + + entities = await hass.async_add_executor_job(get_entities) + + async_add_entities(entities) + + platform = entity_platform.current_platform.get() + assert platform is not None + + platform.async_register_entity_service( + "lock_n_go", + { + vol.Optional(ATTR_UNLATCH, default=False): cv.boolean, + }, + "lock_n_go", ) - devices = [NukiLockEntity(lock) for lock in bridge.locks] - - def service_handler(service): - """Service handler for nuki services.""" - entity_ids = extract_entity_ids(hass, service) - unlatch = service.data[ATTR_UNLATCH] - - for lock in devices: - if lock.entity_id not in entity_ids: - continue - lock.lock_n_go(unlatch=unlatch) - - hass.services.register( - DOMAIN, - SERVICE_LOCK_N_GO, - service_handler, - schema=LOCK_N_GO_SERVICE_SCHEMA, - ) - - devices.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) - - add_entities(devices) - class NukiDeviceEntity(LockEntity, ABC): """Representation of a Nuki device.""" @@ -172,13 +158,13 @@ class NukiLockEntity(NukiDeviceEntity): """Open the door latch.""" self._nuki_device.unlatch() - def lock_n_go(self, unlatch=False, **kwargs): + def lock_n_go(self, unlatch): """Lock and go. This will first unlock the door, then wait for 20 seconds (or another amount of time depending on the lock settings) and relock. """ - self._nuki_device.lock_n_go(unlatch, kwargs) + self._nuki_device.lock_n_go(unlatch) class NukiOpenerEntity(NukiDeviceEntity): @@ -200,3 +186,6 @@ class NukiOpenerEntity(NukiDeviceEntity): def open(self, **kwargs): """Buzz open the door.""" self._nuki_device.electric_strike_actuation() + + def lock_n_go(self, unlatch): + """Stub service.""" From bcebc588a60917ec817ac351ee1a0631b6f30931 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Fri, 11 Dec 2020 21:49:14 +0100 Subject: [PATCH 094/302] Expose spider device information (#44085) * Expose spider device information * Set correct identifiers --- homeassistant/components/spider/climate.py | 10 ++++++++++ homeassistant/components/spider/manifest.json | 2 +- homeassistant/components/spider/switch.py | 14 ++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 7730d8b34c..78764ccf4e 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -44,6 +44,16 @@ class SpiderThermostat(ClimateEntity): self.api = api self.thermostat = thermostat + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.thermostat.id)}, + "name": self.thermostat.name, + "manufacturer": self.thermostat.manufacturer, + "model": self.thermostat.model, + } + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index b285cafcfa..32567e6d13 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -3,7 +3,7 @@ "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", "requirements": [ - "spiderpy==1.3.1" + "spiderpy==1.4.2" ], "codeowners": [ "@peternijssen" diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 1b0c86468e..c9a99f3c20 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -5,7 +5,7 @@ from .const import DOMAIN async def async_setup_entry(hass, config, async_add_entities): - """Initialize a Spider thermostat.""" + """Initialize a Spider Power Plug.""" api = hass.data[DOMAIN][config.entry_id] async_add_entities( [ @@ -19,10 +19,20 @@ class SpiderPowerPlug(SwitchEntity): """Representation of a Spider Power Plug.""" def __init__(self, api, power_plug): - """Initialize the Vera device.""" + """Initialize the Spider Power Plug.""" self.api = api self.power_plug = power_plug + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.power_plug.id)}, + "name": self.power_plug.name, + "manufacturer": self.power_plug.manufacturer, + "model": self.power_plug.model, + } + @property def unique_id(self): """Return the ID of this switch.""" diff --git a/requirements_all.txt b/requirements_all.txt index 79f1472043..f81fbe44f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2087,7 +2087,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.2 # homeassistant.components.spider -spiderpy==1.3.1 +spiderpy==1.4.2 # homeassistant.components.spotcrime spotcrime==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75f8f1cdec..91c387f9d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.2 # homeassistant.components.spider -spiderpy==1.3.1 +spiderpy==1.4.2 # homeassistant.components.spotify spotipy==2.16.1 From 19ff83790ef6a50cc3b4ff12381b32de390a9698 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 12 Dec 2020 00:06:00 +0000 Subject: [PATCH 095/302] [ci skip] Translation update --- homeassistant/components/atag/translations/nl.json | 2 +- homeassistant/components/dsmr/translations/es.json | 4 ++++ homeassistant/components/gios/translations/es.json | 5 +++++ homeassistant/components/homekit/translations/nl.json | 2 +- homeassistant/components/tuya/translations/es.json | 3 +++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 96f135848e..55478f765e 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -12,7 +12,7 @@ "data": { "email": "Email", "host": "Host", - "port": "Poort (10000)" + "port": "Poort " }, "title": "Verbinding maken met het apparaat" } diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 364953d39d..a85293e93a 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -2,6 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "step": { + "one": "Vac\u00edo", + "other": "Vac\u00edo" } }, "options": { diff --git a/homeassistant/components/gios/translations/es.json b/homeassistant/components/gios/translations/es.json index 6888266716..011ddd0b6f 100644 --- a/homeassistant/components/gios/translations/es.json +++ b/homeassistant/components/gios/translations/es.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n del Medio Ambiente de Polonia)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index ca41ff6758..e2607c5f36 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "port_name_in_use": "Er is al een bridge met dezelfde naam of poort geconfigureerd." + "port_name_in_use": "Er is al een bridge of apparaat met dezelfde naam of poort geconfigureerd." }, "step": { "pairing": { diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index 3107b919e5..cd8da78187 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "No se pudo conectar" + }, "error": { "dev_multi_type": "Los m\u00faltiples dispositivos seleccionados para configurar deben ser del mismo tipo", "dev_not_config": "Tipo de dispositivo no configurable", From d1fb554e333c6e436875da6a683030a060d7c071 Mon Sep 17 00:00:00 2001 From: k2v1n58 <38071268+k2v1n58@users.noreply.github.com> Date: Sat, 12 Dec 2020 06:51:57 +0100 Subject: [PATCH 096/302] Add code_arm_required to IFTTT alarm (#43928) * Update alarm_control_panel.py --- .../components/ifttt/alarm_control_panel.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 783cd16fef..519c2e4276 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -52,10 +52,13 @@ DEFAULT_EVENT_HOME = "alarm_arm_home" DEFAULT_EVENT_NIGHT = "alarm_arm_night" DEFAULT_EVENT_DISARM = "alarm_disarm" +CONF_CODE_ARM_REQUIRED = "code_arm_required" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, @@ -76,6 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) code = config.get(CONF_CODE) + code_arm_required = config.get(CONF_CODE_ARM_REQUIRED) event_away = config.get(CONF_EVENT_AWAY) event_home = config.get(CONF_EVENT_HOME) event_night = config.get(CONF_EVENT_NIGHT) @@ -83,7 +87,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): optimistic = config.get(CONF_OPTIMISTIC) alarmpanel = IFTTTAlarmPanel( - name, code, event_away, event_home, event_night, event_disarm, optimistic + name, + code, + code_arm_required, + event_away, + event_home, + event_night, + event_disarm, + optimistic, ) hass.data[DATA_IFTTT_ALARM].append(alarmpanel) add_entities([alarmpanel]) @@ -112,11 +123,20 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): """Representation of an alarm control panel controlled through IFTTT.""" def __init__( - self, name, code, event_away, event_home, event_night, event_disarm, optimistic + self, + name, + code, + code_arm_required, + event_away, + event_home, + event_night, + event_disarm, + optimistic, ): """Initialize the alarm control panel.""" self._name = name self._code = code + self._code_arm_required = code_arm_required self._event_away = event_away self._event_home = event_home self._event_night = event_night @@ -161,19 +181,19 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): def alarm_arm_away(self, code=None): """Send arm away command.""" - if not self._check_code(code): + if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) def alarm_arm_home(self, code=None): """Send arm home command.""" - if not self._check_code(code): + if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) def alarm_arm_night(self, code=None): """Send arm night command.""" - if not self._check_code(code): + if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) From 6d12e764b7dddb9eb5e7b7f889174dad9ff12e46 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Dec 2020 01:17:43 -0800 Subject: [PATCH 097/302] Increase test coverage for nest camera (#44144) --- .coveragerc | 1 - homeassistant/components/nest/camera_sdm.py | 6 +- tests/components/nest/camera_sdm_test.py | 85 +++++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index b267f6967e..9e1ec41500 100644 --- a/.coveragerc +++ b/.coveragerc @@ -578,7 +578,6 @@ omit = homeassistant/components/nest/binary_sensor.py homeassistant/components/nest/camera.py homeassistant/components/nest/camera_legacy.py - homeassistant/components/nest/camera_sdm.py homeassistant/components/nest/climate.py homeassistant/components/nest/climate_legacy.py homeassistant/components/nest/climate_sdm.py diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index cec35eeca2..37bd2fed8a 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -95,9 +95,10 @@ class NestCamera(Camera): @property def supported_features(self): """Flag supported features.""" + supported_features = 0 if CameraLiveStreamTrait.NAME in self._device.traits: - return SUPPORT_STREAM - return 0 + supported_features |= SUPPORT_STREAM + return supported_features async def stream_source(self): """Return the source of the stream.""" @@ -131,7 +132,6 @@ class NestCamera(Camera): if not self._stream: return _LOGGER.debug("Extending stream url") - self._stream_refresh_unsub = None try: self._stream = await self._stream.extend_rtsp_stream() except GoogleNestException as err: diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 4a018305bc..69b413ba51 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -9,9 +9,11 @@ import datetime import aiohttp from google_nest_sdm.device import Device +import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform @@ -140,6 +142,36 @@ async def test_camera_stream(hass, auth): assert image.content == b"image bytes" +async def test_camera_stream_missing_trait(hass, auth): + """Test fetching a video stream when not supported by the API.""" + traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 800, + "height": 600, + } + }, + } + + await async_setup_camera(hass, traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source is None + + # Currently on support getting the image from a live stream + with pytest.raises(HomeAssistantError): + image = await camera.async_get_image(hass, "camera.my_camera") + assert image is None + + async def test_refresh_expired_stream_token(hass, auth): """Test a camera stream expiration and refresh.""" now = utcnow() @@ -220,6 +252,59 @@ async def test_refresh_expired_stream_token(hass, auth): assert stream_source == "rtsp://some/url?auth=g.3.streamingToken" +async def test_stream_response_already_expired(hass, auth): + """Test a API response returning an expired stream url.""" + now = utcnow() + stream_1_expiration = now + datetime.timedelta(seconds=-90) + stream_2_expiration = now + datetime.timedelta(seconds=+90) + auth.responses = [ + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" + }, + "streamExtensionToken": "g.1.extensionToken", + "streamToken": "g.1.streamingToken", + "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), + }, + } + ), + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" + }, + "streamExtensionToken": "g.2.extensionToken", + "streamToken": "g.2.streamingToken", + "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), + }, + } + ), + ] + await async_setup_camera( + hass, + DEVICE_TRAITS, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + # The stream is expired, but we return it anyway + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.1.streamingToken" + + await fire_alarm(hass, now) + + # Second attempt sees that the stream is expired and refreshes + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + async def test_camera_removed(hass, auth): """Test case where entities are removed and stream tokens expired.""" now = utcnow() From 8d9fd93dc7575a489d8a0782aa337b213ffb62b7 Mon Sep 17 00:00:00 2001 From: finity69x2 <32221243+finity69x2@users.noreply.github.com> Date: Sat, 12 Dec 2020 04:29:41 -0500 Subject: [PATCH 098/302] Update strings.json to clarify the requirements for the API key (#44143) --- homeassistant/components/nws/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nws/strings.json b/homeassistant/components/nws/strings.json index 0f0bdcf4a1..0f119e7c2e 100644 --- a/homeassistant/components/nws/strings.json +++ b/homeassistant/components/nws/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.", + "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station. For now, an API Key can be anything. It is recommended to use a valid email address.", "title": "Connect to the National Weather Service", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", From 06967245066deb4c387c6e2f3fa06a5e0278a699 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 12 Dec 2020 02:43:38 -0700 Subject: [PATCH 099/302] Fix inability to erase SimpliSafe code (#44137) --- .../components/simplisafe/config_flow.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index c34255bc62..f17a2ce2e4 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -15,6 +15,15 @@ from homeassistant.helpers import aiohttp_client from . import async_get_client_id from .const import DOMAIN, LOGGER # pylint: disable=unused-import +FULL_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CODE): str, + } +) +PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -24,15 +33,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.full_data_schema = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CODE): str, - } - ) - self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str}) - self._code = None self._password = None self._username = None @@ -125,21 +125,19 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle re-auth completion.""" if not user_input: return self.async_show_form( - step_id="reauth_confirm", data_schema=self.password_data_schema + step_id="reauth_confirm", data_schema=PASSWORD_DATA_SCHEMA ) self._password = user_input[CONF_PASSWORD] return await self._async_login_during_step( - step_id="reauth_confirm", form_schema=self.password_data_schema + step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA ) async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" if not user_input: - return self.async_show_form( - step_id="user", data_schema=self.full_data_schema - ) + return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() @@ -149,7 +147,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] return await self._async_login_during_step( - step_id="user", form_schema=self.full_data_schema + step_id="user", form_schema=FULL_DATA_SCHEMA ) @@ -171,7 +169,9 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_CODE, - default=self.config_entry.options.get(CONF_CODE), + description={ + "suggested_value": self.config_entry.options.get(CONF_CODE) + }, ): str } ), From dd4147fbaaa41c23ee4695fdce65fcd8ab2f9a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Sat, 12 Dec 2020 18:44:45 +0100 Subject: [PATCH 100/302] Log unique_id of device when ESPHome connection fails (#44152) --- homeassistant/components/esphome/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a12754a87f..fcfb4cf7ff 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -257,7 +257,12 @@ async def _setup_auto_reconnect_logic( try: await cli.connect(on_stop=try_connect, login=True) except APIConnectionError as error: - _LOGGER.info("Can't connect to ESPHome API for %s: %s", host, error) + _LOGGER.info( + "Can't connect to ESPHome API for %s (%s): %s", + entry.unique_id, + host, + error, + ) # Schedule re-connect in event loop in order not to delay HA # startup. First connect is scheduled in tracked tasks. data.reconnect_task = hass.loop.create_task( From 8fe5e61cbf871649c21fce2dd1678731267dec8b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 12 Dec 2020 10:41:20 -0800 Subject: [PATCH 101/302] Test edge cases in wemo platform code (#44136) --- homeassistant/components/wemo/light.py | 2 +- tests/components/wemo/conftest.py | 5 +- tests/components/wemo/entity_test_helpers.py | 167 +++++++++++++++++++ tests/components/wemo/test_binary_sensor.py | 22 +++ tests/components/wemo/test_fan.py | 22 +++ tests/components/wemo/test_light_bridge.py | 68 +++++++- tests/components/wemo/test_light_dimmer.py | 22 +++ tests/components/wemo/test_switch.py | 22 +++ 8 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 tests/components/wemo/entity_test_helpers.py diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 5d4aa18ee2..4c88c157f9 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -208,7 +208,7 @@ class WemoLight(LightEntity): except (AttributeError, ActionException) as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.reconnect_with_device() + self.wemo.bridge.reconnect_with_device() else: self._is_on = self._state.get("onoff") != WEMO_OFF self._brightness = self._state.get("level", 255) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 78262ccc59..8eb8b0d9a3 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -23,7 +23,7 @@ def pywemo_model_fixture(): @pytest.fixture(name="pywemo_registry") def pywemo_registry_fixture(): """Fixture for SubscriptionRegistry instances.""" - registry = create_autospec(pywemo.SubscriptionRegistry) + registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) registry.callbacks = {} @@ -39,12 +39,13 @@ def pywemo_registry_fixture(): @pytest.fixture(name="pywemo_device") def pywemo_device_fixture(pywemo_registry, pywemo_model): """Fixture for WeMoDevice instances.""" - device = create_autospec(getattr(pywemo, pywemo_model)) + device = create_autospec(getattr(pywemo, pywemo_model), instance=True) device.host = MOCK_HOST device.port = MOCK_PORT device.name = MOCK_NAME device.serialnumber = MOCK_SERIAL_NUMBER device.model_name = pywemo_model + device.get_state.return_value = 0 # Default to Off url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" with patch("pywemo.setup_url_for_address", return_value=url), patch( diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py new file mode 100644 index 0000000000..16a2f8b3f0 --- /dev/null +++ b/tests/components/wemo/entity_test_helpers.py @@ -0,0 +1,167 @@ +"""Test cases that are in common among wemo platform modules. + +This is not a test module. These test methods are used by the platform test modules. +""" +import asyncio +import threading + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch + + +def _perform_registry_callback(hass, pywemo_registry, pywemo_device): + """Return a callable method to trigger a state callback from the device.""" + + @callback + def async_callback(): + # Cause a state update callback to be triggered by the device. + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + return hass.async_block_till_done() + + return async_callback + + +def _perform_async_update(hass, wemo_entity): + """Return a callable method to cause hass to update the state of the entity.""" + + @callback + def async_callback(): + return hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + return async_callback + + +async def _async_multiple_call_helper( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + call1, + call2, + update_polling_method=None, +): + """Create two calls (call1 & call2) in parallel; verify only one polls the device. + + The platform entity should only perform one update poll on the device at a time. + Any parallel updates that happen at the same time should be ignored. This is + verified by blocking in the update polling method. The polling method should + only be called once as a result of calling call1 & call2 simultaneously. + """ + # get_state is called outside the event loop. Use non-async Python Event. + event = threading.Event() + + def get_update(force_update=True): + event.wait() + + update_polling_method = update_polling_method or pywemo_device.get_state + update_polling_method.side_effect = get_update + + # One of these two calls will block on `event`. The other will return right + # away because the `_update_lock` is held. + _, pending = await asyncio.wait( + [call1(), call2()], return_when=asyncio.FIRST_COMPLETED + ) + + # Allow the blocked call to return. + event.set() + if pending: + await asyncio.wait(pending) + + # Make sure the state update only happened once. + update_polling_method.assert_called_once() + + +async def test_async_update_locked_callback_and_update( + hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs +): + """Test that a callback and a state update request can't both happen at the same time. + + When a state update is received via a callback from the device at the same time + as hass is calling `async_update`, verify that only one of the updates proceeds. + """ + await async_setup_component(hass, HA_DOMAIN, {}) + callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device) + update = _perform_async_update(hass, wemo_entity) + await _async_multiple_call_helper( + hass, pywemo_registry, wemo_entity, pywemo_device, callback, update, **kwargs + ) + + +async def test_async_update_locked_multiple_updates( + hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs +): + """Test that two hass async_update state updates do not proceed at the same time.""" + await async_setup_component(hass, HA_DOMAIN, {}) + update = _perform_async_update(hass, wemo_entity) + await _async_multiple_call_helper( + hass, pywemo_registry, wemo_entity, pywemo_device, update, update, **kwargs + ) + + +async def test_async_update_locked_multiple_callbacks( + hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs +): + """Test that two device callback state updates do not proceed at the same time.""" + await async_setup_component(hass, HA_DOMAIN, {}) + callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device) + await _async_multiple_call_helper( + hass, pywemo_registry, wemo_entity, pywemo_device, callback, callback, **kwargs + ) + + +async def test_async_locked_update_with_exception( + hass, wemo_entity, pywemo_device, update_polling_method=None +): + """Test that the entity becomes unavailable when communication is lost.""" + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + await async_setup_component(hass, HA_DOMAIN, {}) + update_polling_method = update_polling_method or pywemo_device.get_state + update_polling_method.side_effect = AttributeError + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + pywemo_device.reconnect_with_device.assert_called_with() + + +async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_device): + """Test that the entity becomes unavailable after a timeout, and that it recovers.""" + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + await async_setup_component(hass, HA_DOMAIN, {}) + + with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError): + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + + # Check that the entity recovers and is available after the update succeeds. + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_binary_sensor.py b/tests/components/wemo/test_binary_sensor.py index f217d0b168..1bf6f0f3be 100644 --- a/tests/components/wemo/test_binary_sensor.py +++ b/tests/components/wemo/test_binary_sensor.py @@ -9,6 +9,8 @@ from homeassistant.components.homeassistant import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component +from . import entity_test_helpers + @pytest.fixture def pywemo_model(): @@ -16,6 +18,26 @@ def pywemo_model(): return "Motion" +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (Motion). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + async def test_binary_sensor_registry_state_callback( hass, pywemo_registry, pywemo_device, wemo_entity ): diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index ed49519c77..fe7298b40c 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -11,6 +11,8 @@ from homeassistant.components.wemo.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component +from . import entity_test_helpers + @pytest.fixture def pywemo_model(): @@ -18,6 +20,26 @@ def pywemo_model(): return "Humidifier" +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (Humidifier). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + async def test_fan_registry_state_callback( hass, pywemo_registry, pywemo_device, wemo_entity ): diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index d76c714ba5..1a36e5421e 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -1,5 +1,4 @@ """Tests for the Wemo light entity via the bridge.""" - import pytest import pywemo @@ -7,10 +6,14 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) +from homeassistant.components.wemo.light import MIN_TIME_BETWEEN_SCANS from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.async_mock import PropertyMock, create_autospec +from . import entity_test_helpers + +from tests.async_mock import create_autospec, patch @pytest.fixture @@ -19,16 +22,71 @@ def pywemo_model(): return "Bridge" +# Note: The ordering of where the pywemo_bridge_light comes in test arguments matters. +# In test methods, the pywemo_bridge_light fixture argument must come before the +# wemo_entity fixture argument. @pytest.fixture(name="pywemo_bridge_light") def pywemo_bridge_light_fixture(pywemo_device): """Fixture for Bridge.Light WeMoDevice instances.""" - light = create_autospec(pywemo.ouimeaux_device.bridge.Light) + light = create_autospec(pywemo.ouimeaux_device.bridge.Light, instance=True) light.uniqueID = pywemo_device.serialnumber light.name = pywemo_device.name + light.bridge = pywemo_device + light.state = {"onoff": 0} pywemo_device.Lights = {pywemo_device.serialnumber: light} return light +def _bypass_throttling(): + """Bypass the util.Throttle on the update_lights method.""" + utcnow = dt_util.utcnow() + + def increment_and_return_time(): + nonlocal utcnow + utcnow += MIN_TIME_BETWEEN_SCANS + return utcnow + + return patch("homeassistant.util.utcnow", side_effect=increment_and_return_time) + + +async def test_async_update_locked_multiple_updates( + hass, pywemo_registry, pywemo_bridge_light, wemo_entity, pywemo_device +): + """Test that two state updates do not proceed at the same time.""" + pywemo_device.bridge_update.reset_mock() + + with _bypass_throttling(): + await entity_test_helpers.test_async_update_locked_multiple_updates( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.bridge_update, + ) + + +async def test_async_update_with_timeout_and_recovery( + hass, pywemo_bridge_light, wemo_entity, pywemo_device +): + """Test that the entity becomes unavailable after a timeout, and that it recovers.""" + await entity_test_helpers.test_async_update_with_timeout_and_recovery( + hass, wemo_entity, pywemo_device + ) + + +async def test_async_locked_update_with_exception( + hass, pywemo_bridge_light, wemo_entity, pywemo_device +): + """Test that the entity becomes unavailable when communication is lost.""" + with _bypass_throttling(): + await entity_test_helpers.test_async_locked_update_with_exception( + hass, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.bridge_update, + ) + + async def test_light_update_entity( hass, pywemo_registry, pywemo_bridge_light, wemo_entity ): @@ -36,7 +94,7 @@ async def test_light_update_entity( await async_setup_component(hass, HA_DOMAIN, {}) # On state. - type(pywemo_bridge_light).state = PropertyMock(return_value={"onoff": 1}) + pywemo_bridge_light.state = {"onoff": 1} await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -46,7 +104,7 @@ async def test_light_update_entity( assert hass.states.get(wemo_entity.entity_id).state == STATE_ON # Off state. - type(pywemo_bridge_light).state = PropertyMock(return_value={"onoff": 0}) + pywemo_bridge_light.state = {"onoff": 0} await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, diff --git a/tests/components/wemo/test_light_dimmer.py b/tests/components/wemo/test_light_dimmer.py index e94634c6d1..45fdd01a64 100644 --- a/tests/components/wemo/test_light_dimmer.py +++ b/tests/components/wemo/test_light_dimmer.py @@ -9,6 +9,8 @@ from homeassistant.components.homeassistant import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component +from . import entity_test_helpers + @pytest.fixture def pywemo_model(): @@ -16,6 +18,26 @@ def pywemo_model(): return "Dimmer" +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (Dimmer). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + async def test_light_registry_state_callback( hass, pywemo_registry, pywemo_device, wemo_entity ): diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py index 1ae8e3f945..05151d38be 100644 --- a/tests/components/wemo/test_switch.py +++ b/tests/components/wemo/test_switch.py @@ -9,6 +9,8 @@ from homeassistant.components.homeassistant import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component +from . import entity_test_helpers + @pytest.fixture def pywemo_model(): @@ -16,6 +18,26 @@ def pywemo_model(): return "LightSwitch" +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (LightSwitch). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + async def test_switch_registry_state_callback( hass, pywemo_registry, pywemo_device, wemo_entity ): From 05f9fb80c83c3e5c49844f1c365ce105c01cc870 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 12 Dec 2020 19:47:46 +0100 Subject: [PATCH 102/302] Fix upnp first discovered device is used (#44151) Co-authored-by: Martin Hjelmare --- homeassistant/components/upnp/__init__.py | 14 ++++++++++++-- homeassistant/components/upnp/config_flow.py | 1 + homeassistant/components/upnp/device.py | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 773a872f33..c9f96a0e9d 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -43,6 +43,8 @@ async def async_discover_and_construct( ) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name + _LOGGER.debug("Constructing device: %s::%s", udn, st) + discovery_infos = await Device.async_discover(hass) _LOGGER.debug("Discovered devices: %s", discovery_infos) if not discovery_infos: @@ -53,7 +55,7 @@ async def async_discover_and_construct( # Get the discovery info with specified UDN/ST. filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn] if st: - filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st] + filtered = [di for di in filtered if di[DISCOVERY_ST] == st] if not filtered: _LOGGER.warning( 'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting', @@ -74,6 +76,7 @@ async def async_discover_and_construct( ) _LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name) + _LOGGER.debug("Constructing from discovery_info: %s", discovery_info) location = discovery_info[DISCOVERY_LOCATION] return await Device.async_create_device(hass, location) @@ -104,7 +107,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data) + _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) # Discover and construct. udn = config_entry.data.get(CONFIG_ENTRY_UDN) @@ -123,6 +126,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Ensure entry has a unique_id. if not config_entry.unique_id: + _LOGGER.debug( + "Setting unique_id: %s, for config_entry: %s", + device.unique_id, + config_entry, + ) hass.config_entries.async_update_entry( entry=config_entry, unique_id=device.unique_id, @@ -152,6 +160,8 @@ async def async_unload_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: """Unload a UPnP/IGD device from a config entry.""" + _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) + udn = config_entry.data.get(CONFIG_ENTRY_UDN) if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: del hass.data[DOMAIN][DOMAIN_DEVICES][udn] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 72efc4ffd5..7b20c7709a 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -154,6 +154,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() # Store discovery. + _LOGGER.debug("New discovery, continuing") name = discovery_info.get("friendlyName", "") discovery = { DISCOVERY_UDN: udn, diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5f29043a1f..6bc497170c 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -109,7 +109,7 @@ class Device: def __str__(self) -> str: """Get string representation.""" - return f"IGD Device: {self.name}/{self.udn}" + return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" async def async_get_traffic_data(self) -> Mapping[str, any]: """ From 1a8123aba67a50cc228d5b74ec17c76d99770c9f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Dec 2020 11:57:02 -0800 Subject: [PATCH 103/302] Increase nest climate test coverage (#44146) --- .coveragerc | 1 - homeassistant/components/nest/climate_sdm.py | 23 +- tests/components/nest/climate_sdm_test.py | 284 +++++++++++++++++++ 3 files changed, 290 insertions(+), 18 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9e1ec41500..01c0a657f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -580,7 +580,6 @@ omit = homeassistant/components/nest/camera_legacy.py homeassistant/components/nest/climate.py homeassistant/components/nest/climate_legacy.py - homeassistant/components/nest/climate_sdm.py homeassistant/components/nest/local_auth.py homeassistant/components/nest/sensor.py homeassistant/components/nest/sensor_legacy.py diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e56d35c1df..fbaeb502b4 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -186,8 +186,6 @@ class ThermostatEntity(ClimateEntity): @property def _target_temperature_trait(self): """Return the correct trait with a target temp depending on mode.""" - if not self.hvac_mode: - return None if self.preset_mode == PRESET_ECO: if ThermostatEcoTrait.NAME in self._device.traits: return self._device.traits[ThermostatEcoTrait.NAME] @@ -225,8 +223,6 @@ class ThermostatEntity(ClimateEntity): @property def hvac_action(self): """Return the current HVAC action (heating, cooling).""" - if ThermostatHvacTrait.NAME not in self._device.traits: - return None trait = self._device.traits[ThermostatHvacTrait.NAME] if trait.status in THERMOSTAT_HVAC_STATUS_MAP: return THERMOSTAT_HVAC_STATUS_MAP[trait.status] @@ -262,9 +258,10 @@ class ThermostatEntity(ClimateEntity): @property def fan_modes(self): """Return the list of available fan modes.""" + modes = [] if FanTrait.NAME in self._device.traits: - return list(FAN_INV_MODE_MAP) - return [] + modes = list(FAN_INV_MODE_MAP) + return modes @property def supported_features(self): @@ -290,12 +287,8 @@ class ThermostatEntity(ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: - return - if hvac_mode not in THERMOSTAT_INV_MODE_MAP: - return + raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] - if ThermostatModeTrait.NAME not in self._device.traits: - return trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) @@ -318,17 +311,13 @@ class ThermostatEntity(ClimateEntity): async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" if preset_mode not in self.preset_modes: - return - if ThermostatEcoTrait.NAME not in self._device.traits: - return + raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode not in self.fan_modes: - return - if FanTrait.NAME not in self._device.traits: - return + raise ValueError(f"Unsupported fan__mode '{fan_mode}'") trait = self._device.traits[FanTrait.NAME] await trait.set_timer(FAN_INV_MODE_MAP[fan_mode]) diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index bf6716ec96..886b67f8e2 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -7,6 +7,7 @@ pubsub subscriber. from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage +import pytest from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, @@ -21,14 +22,17 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_OFF, + FAN_LOW, FAN_OFF, FAN_ON, HVAC_MODE_COOL, + HVAC_MODE_DRY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, + PRESET_SLEEP, ) from homeassistant.const import ATTR_TEMPERATURE @@ -450,6 +454,34 @@ async def test_thermostat_set_hvac_mode(hass, auth): assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT +async def test_thermostat_invalid_hvac_mode(hass, auth): + """Test setting an hvac_mode that is not supported.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + with pytest.raises(ValueError): + await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await hass.async_block_till_done() + + assert thermostat.state == HVAC_MODE_OFF + assert auth.method is None # No communication with API + + async def test_thermostat_set_eco_preset(hass, auth): """Test a thermostat put into eco mode.""" subscriber = await setup_climate( @@ -782,6 +814,53 @@ async def test_thermostat_fan_empty(hass): assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes + # Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_invalid_fan_mode(hass): + """Test setting a fan mode that is not supported.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 16.2, + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + with pytest.raises(ValueError): + await common.async_set_fan_mode(hass, FAN_LOW) + await hass.async_block_till_done() + async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" @@ -843,3 +922,208 @@ async def test_thermostat_target_temp(hass, auth): assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0 assert thermostat.attributes[ATTR_TEMPERATURE] is None + + +async def test_thermostat_missing_mode_traits(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() + assert ATTR_TEMPERATURE not in thermostat.attributes + assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes + assert ATTR_TARGET_TEMP_HIGH not in thermostat.attributes + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + await common.async_set_temperature(hass, temperature=24.0) + await hass.async_block_till_done() + assert ATTR_TEMPERATURE not in thermostat.attributes + + await common.async_set_preset_mode(hass, PRESET_ECO) + await hass.async_block_till_done() + assert ATTR_PRESET_MODE not in thermostat.attributes + + +async def test_thermostat_missing_temperature_trait(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEAT", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + await common.async_set_temperature(hass, temperature=24.0) + await hass.async_block_till_done() + assert thermostat.attributes[ATTR_TEMPERATURE] is None + + +async def test_thermostat_unexpected_hvac_status(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "UNEXPECTED"}, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert ATTR_HVAC_ACTION not in thermostat.attributes + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() + assert ATTR_TEMPERATURE not in thermostat.attributes + assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes + assert ATTR_TARGET_TEMP_HIGH not in thermostat.attributes + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + with pytest.raises(ValueError): + await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await hass.async_block_till_done() + assert thermostat.state == HVAC_MODE_OFF + + +async def test_thermostat_missing_set_point(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEATCOOL", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_unexepected_hvac_mode(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF", "UNEXPECTED"], + "mode": "UNEXPECTED", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_invalid_set_preset_mode(hass, auth): + """Test a thermostat set with an invalid preset mode.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["MANUAL_ECO", "OFF"], + "mode": "OFF", + "heatCelsius": 15.0, + "coolCelsius": 28.0, + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] + + # Set preset mode that is invalid + with pytest.raises(ValueError): + await common.async_set_preset_mode(hass, PRESET_SLEEP) + await hass.async_block_till_done() + + # No RPC sent + assert auth.method is None + + # Preset is unchanged + assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] From 76d6b55ff451c279c9136af3140fbc5b4dfc58b2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Dec 2020 21:34:16 +0100 Subject: [PATCH 104/302] Updated frontend to 20201212.0 (#44154) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cb52afd4b6..caf309e671 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201210.0"], + "requirements": ["home-assistant-frontend==20201212.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e69ae0c9e..180305e8ed 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index f81fbe44f1..855cfbc311 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91c387f9d4..7da703b01d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 594e905742c6e934f17eb4c7ad94053ed0dc946e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Dec 2020 22:24:16 +0100 Subject: [PATCH 105/302] Remove invalidation_version from deprecated (#44156) * Remove invalidation_version from deprecated. We don't follow up and just hurts releases * Revert change to ZHA --- .../components/arcam_fmj/__init__.py | 2 +- homeassistant/components/automation/config.py | 2 +- homeassistant/components/canary/camera.py | 2 +- .../components/cloudflare/__init__.py | 8 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/directv/__init__.py | 2 +- .../components/flunearyou/__init__.py | 2 +- homeassistant/components/hyperion/light.py | 6 +- homeassistant/components/local_ip/__init__.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/notion/__init__.py | 2 +- .../components/rainmachine/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/sentry/__init__.py | 2 +- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/withings/__init__.py | 2 +- homeassistant/components/zha/core/device.py | 2 + homeassistant/helpers/config_validation.py | 39 +--- tests/helpers/test_config_validation.py | 174 ------------------ tests/test_config.py | 2 +- 20 files changed, 25 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 0875e09435..0175dfd658 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -23,7 +23,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def _await_cancel(task): diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 8a13334bc6..9c26f3552a 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -32,7 +32,7 @@ from .helpers import async_get_blueprints _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), + cv.deprecated(CONF_HIDE_ENTITY), script.make_script_schema( { # str on purpose diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index c3fd2a6ff0..fd2f08c148 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -30,7 +30,7 @@ from .coordinator import CanaryDataUpdateCoordinator MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"), + cv.deprecated(CONF_FFMPEG_ARGUMENTS), PLATFORM_SCHEMA.extend( { vol.Optional( diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 3ebb919393..446890887c 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -33,10 +33,10 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_EMAIL, invalidation_version="0.119"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.119"), - cv.deprecated(CONF_ZONE, invalidation_version="0.119"), - cv.deprecated(CONF_RECORDS, invalidation_version="0.119"), + cv.deprecated(CONF_EMAIL), + cv.deprecated(CONF_API_KEY), + cv.deprecated(CONF_ZONE), + cv.deprecated(CONF_RECORDS), vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 7b9c1ded67..b4950b8b05 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -30,7 +30,7 @@ COMPONENT_TYPES = ["climate", "sensor", "switch"] CONFIG_SCHEMA = vol.Schema( vol.All( - cv.deprecated(DOMAIN, invalidation_version="0.113.0"), + cv.deprecated(DOMAIN), { DOMAIN: vol.Schema( { diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 59682178d4..22a97b9e82 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -22,7 +22,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 7399dd3847..46442f112b 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -20,7 +20,7 @@ from .const import ( DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["sensor"] diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index b8e9040f7c..5aa087c051 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -80,13 +80,13 @@ SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT # Usage of YAML for configuration of the Hyperion component is deprecated. PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"), + cv.deprecated(CONF_HDMI_PRIORITY), cv.deprecated(CONF_HOST), cv.deprecated(CONF_PORT), - cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"), + cv.deprecated(CONF_DEFAULT_COLOR), cv.deprecated(CONF_NAME), cv.deprecated(CONF_PRIORITY), - cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"), + cv.deprecated(CONF_EFFECT_LIST), PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index f787c02876..637520aa30 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -11,7 +11,7 @@ from .const import DOMAIN, PLATFORM CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_NAME, invalidation_version="0.110"), + cv.deprecated(CONF_NAME), vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string}), ) }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 73caf023ef..cced3670cc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -191,7 +191,7 @@ def embedded_broker_deprecated(value): CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_TLS_VERSION, invalidation_version="0.115"), + cv.deprecated(CONF_TLS_VERSION), vol.Schema( { vol.Optional(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 561f3edf89..88da19f5ab 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -30,7 +30,7 @@ ATTR_SYSTEM_NAME = "system_name" DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a520772ff7..41c56e38db 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -74,7 +74,7 @@ SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 739e345a63..af2e0ee946 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index eecac0281e..6be02b9ba5 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -33,7 +33,7 @@ from .const import ( ENTITY_COMPONENTS, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.117") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2430aad43c..89f5c40b1f 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -138,7 +138,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( } ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) @callback diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 94795a10c8..c6f420d172 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -40,7 +40,7 @@ DOMAIN = const.DOMAIN CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(const.CONF_PROFILES, invalidation_version="0.114"), + cv.deprecated(const.CONF_PROFILES), vol.Schema( { vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 81b522308f..cd3b1bd93c 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -254,8 +254,10 @@ class ZHADevice(LogMixin): "device_event_type": "device_offline" } } + if hasattr(self._zigpy_device, "device_automation_triggers"): triggers.update(self._zigpy_device.device_automation_triggers) + return triggers @property diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index dea8deec71..0513c5c6e7 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -28,7 +28,6 @@ from typing import ( from urllib.parse import urlparse from uuid import UUID -from pkg_resources import parse_version import voluptuous as vol import voluptuous_serialize @@ -80,7 +79,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, - __version__, ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError @@ -712,7 +710,6 @@ class multi_select: def deprecated( key: str, replacement_key: Optional[str] = None, - invalidation_version: Optional[str] = None, default: Optional[Any] = None, ) -> Callable[[Dict], Dict]: """ @@ -725,8 +722,6 @@ def deprecated( - No warning if only replacement_key provided - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected """ module = inspect.getmodule(inspect.stack()[1][0]) if module is not None: @@ -737,56 +732,24 @@ def deprecated( # https://github.com/home-assistant/core/issues/24982 module_name = __name__ - if replacement_key and invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please replace it with '{replacement_key}'." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) - elif replacement_key: + if replacement_key: warning = ( "The '{key}' option is deprecated," " please replace it with '{replacement_key}'" ) - elif invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please remove it from your configuration." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) else: warning = ( "The '{key}' option is deprecated," " please remove it from your configuration" ) - def check_for_invalid_version() -> None: - """Raise error if current version has reached invalidation.""" - if not invalidation_version: - return - - if parse_version(__version__) >= parse_version(invalidation_version): - raise vol.Invalid( - warning.format( - key=key, - replacement_key=replacement_key, - invalidation_status="became", - invalidation_version=invalidation_version, - ) - ) - def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: - check_for_invalid_version() KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning, key=key, replacement_key=replacement_key, - invalidation_status="will become", - invalidation_version=invalidation_version, ) value = config[key] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 480b2280af..5d907408b6 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -698,116 +698,6 @@ def test_deprecated_with_replacement_key(caplog, schema): assert test_data == output -def test_deprecated_with_invalidation_version(caplog, schema, version): - """ - Test deprecation behaves correctly with only an invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema without changing any values - - No warning or difference in output if key is not provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated("mars", invalidation_version="9999.99.9"), schema - ) - - message = ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. " - "This option will become invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert message in caplog.text - assert test_data == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"venus": False} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", invalidation_version="0.1.0"), schema - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. This option became " - "invalid in version 0.1.0" - ) - - -def test_deprecated_with_replacement_key_and_invalidation_version( - caplog, schema, version -): - """ - Test deprecation behaves with a replacement key & invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning or difference in output if neither key nor - replacement_key are provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", replacement_key="jupiter", invalidation_version="9999.99.9" - ), - schema, - ) - - warning = ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert warning in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_with_default(caplog, schema): """ Test deprecation behaves correctly with a default value. @@ -894,69 +784,6 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): assert {"jupiter": True} == output -def test_deprecated_with_replacement_key_invalidation_version_default( - caplog, schema, version -): - """ - Test deprecation with a replacement key, invalidation_version & default. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning if neither key nor replacement_key are provided - - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", - replacement_key="jupiter", - invalidation_version="9999.99.9", - default=False, - ), - schema, - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert {"venus": True, "jupiter": False} == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_cant_find_module(): """Test if the current module cannot be inspected.""" with patch("inspect.getmodule", return_value=None): @@ -964,7 +791,6 @@ def test_deprecated_cant_find_module(): cv.deprecated( "mars", replacement_key="jupiter", - invalidation_version="1.0.0", default=False, ) diff --git a/tests/test_config.py b/tests/test_config.py index bfda156f2b..931b672d01 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1116,7 +1116,7 @@ async def test_component_config_exceptions(hass, caplog): ("non_existing", vol.Schema({"zone": int}), None), ("zone", vol.Schema({}), None), ("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"), - ("openuv", cv.deprecated("openuv", invalidation_version="0.115"), None), + ("openuv", cv.deprecated("openuv"), None), ], ) def test_identify_config_schema(domain, schema, expected): From a3a7ce48427f943446f453a4ada3dacb4f4406f4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 13 Dec 2020 00:04:18 +0000 Subject: [PATCH 106/302] [ci skip] Translation update --- homeassistant/components/nws/translations/ca.json | 2 +- homeassistant/components/nws/translations/en.json | 2 +- homeassistant/components/nws/translations/et.json | 2 +- homeassistant/components/nws/translations/ru.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nws/translations/ca.json b/homeassistant/components/nws/translations/ca.json index e012d68a10..4d6e23022a 100644 --- a/homeassistant/components/nws/translations/ca.json +++ b/homeassistant/components/nws/translations/ca.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "Codi d'estaci\u00f3 METAR" }, - "description": "Si no s'especifica un codi d'estaci\u00f3 METAR, la latitud i longitud s'utilitzaran per trobar l'estaci\u00f3 m\u00e9s propera.", + "description": "Si no s'especifica un codi d'estaci\u00f3 METAR, s'utilitzaran la latitud i longitud per trobar l'estaci\u00f3 m\u00e9s propera. De moment, la clau d'API pot ser qualsevol cosa. Es recomana utilitzar una adre\u00e7a de correu electr\u00f2nic v\u00e0lida.", "title": "Connexi\u00f3 amb el Servei Meteorol\u00f2gic Nacional (USA)" } } diff --git a/homeassistant/components/nws/translations/en.json b/homeassistant/components/nws/translations/en.json index 04cb13bf5e..211f35d62c 100644 --- a/homeassistant/components/nws/translations/en.json +++ b/homeassistant/components/nws/translations/en.json @@ -15,7 +15,7 @@ "longitude": "Longitude", "station": "METAR station code" }, - "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.", + "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station. For now, an API Key can be anything. It is recommended to use a valid email address.", "title": "Connect to the National Weather Service" } } diff --git a/homeassistant/components/nws/translations/et.json b/homeassistant/components/nws/translations/et.json index 4ef73de723..3fe3d33793 100644 --- a/homeassistant/components/nws/translations/et.json +++ b/homeassistant/components/nws/translations/et.json @@ -15,7 +15,7 @@ "longitude": "Pikkuskraad", "station": "METAR jaamakood" }, - "description": "Kui METAR-i jaamakoodi pole m\u00e4\u00e4ratud, kasutatakse l\u00e4hima jaama leidmiseks laius- ja pikkuskraadi.", + "description": "Kui METAR-i jaamakoodi pole m\u00e4\u00e4ratud, kasutatakse l\u00e4hima jaama leidmiseks laius- ja pikkuskraadi. API v\u00f5ti on hetkel suvaline. Sovitatav on kasutada kehtivat e-kirja aadressi.", "title": "\u00dchendu riikliku ilmateenistusega (USA)" } } diff --git a/homeassistant/components/nws/translations/ru.json b/homeassistant/components/nws/translations/ru.json index 926e936a59..bc600f8428 100644 --- a/homeassistant/components/nws/translations/ru.json +++ b/homeassistant/components/nws/translations/ru.json @@ -15,7 +15,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "station": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR" }, - "description": "\u0415\u0441\u043b\u0438 \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u0430.", + "description": "\u0415\u0441\u043b\u0438 \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u0430. \u041d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043b\u044e\u0431\u044b\u043c. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b.", "title": "National Weather Service" } } From 82558156886fb2641a5be064ef7d824ffa2149a2 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 13 Dec 2020 03:02:45 -0800 Subject: [PATCH 107/302] Share wemo entity code to reduce duplicate boilerplate (#44113) --- .../components/wemo/binary_sensor.py | 97 +------------ homeassistant/components/wemo/entity.py | 124 ++++++++++++++++ homeassistant/components/wemo/fan.py | 95 +----------- homeassistant/components/wemo/light.py | 135 +----------------- homeassistant/components/wemo/switch.py | 105 ++------------ 5 files changed, 144 insertions(+), 412 deletions(-) create mode 100644 homeassistant/components/wemo/entity.py diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 44031e846c..b6690ed6d2 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -2,13 +2,13 @@ import asyncio import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoSubscriptionEntity _LOGGER = logging.getLogger(__name__) @@ -30,72 +30,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoBinarySensor(BinarySensorEntity): +class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): """Representation a WeMo binary sensor.""" - def __init__(self, device): - """Initialize the WeMo sensor.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serial_number = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo sensor.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Wemo sensor added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo sensor removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo sensor is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - def _update(self, force_update=True): """Update the sensor state.""" try: @@ -108,33 +45,3 @@ class WemoBinarySensor(BinarySensorEntity): _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() - - @property - def unique_id(self): - """Return the id of this WeMo sensor.""" - return self._serial_number - - @property - def name(self): - """Return the name of the service if any.""" - return self._name - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - @property - def available(self): - """Return true if sensor is available.""" - return self._available - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serial_number)}, - "model": self._model_name, - "manufacturer": "Belkin", - } diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py new file mode 100644 index 0000000000..e7c0712272 --- /dev/null +++ b/homeassistant/components/wemo/entity.py @@ -0,0 +1,124 @@ +"""Classes shared among Wemo entities.""" +import asyncio +import logging +from typing import Any, Dict, Optional + +import async_timeout +from pywemo import WeMoDevice + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN as WEMO_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class WemoEntity(Entity): + """Common methods for Wemo entities. + + Requires that subclasses implement the _update method. + """ + + def __init__(self, device: WeMoDevice) -> None: + """Initialize the WeMo device.""" + self.wemo = device + self._state = None + self._available = True + self._update_lock = None + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return self.wemo.name + + @property + def available(self) -> bool: + """Return true if switch is available.""" + return self._available + + def _update(self, force_update: Optional[bool] = True): + """Update the device state.""" + raise NotImplementedError() + + async def async_added_to_hass(self) -> None: + """Wemo device added to Home Assistant.""" + # Define inside async context so we know our event loop + self._update_lock = asyncio.Lock() + + async def async_update(self) -> None: + """Update WeMo state. + + Wemo has an aggressive retry logic that sometimes can take over a + minute to return. If we don't get a state after 5 seconds, assume the + Wemo switch is unreachable. If update goes through, it will be made + available again. + """ + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + try: + with async_timeout.timeout(5): + await asyncio.shield(self._async_locked_update(True)) + except asyncio.TimeoutError: + _LOGGER.warning("Lost connection to %s", self.name) + self._available = False + + async def _async_locked_update(self, force_update: bool) -> None: + """Try updating within an async lock.""" + async with self._update_lock: + await self.hass.async_add_executor_job(self._update, force_update) + + +class WemoSubscriptionEntity(WemoEntity): + """Common methods for Wemo devices that register for update callbacks.""" + + @property + def unique_id(self) -> str: + """Return the id of this WeMo device.""" + return self.wemo.serialnumber + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device info.""" + return { + "name": self.name, + "identifiers": {(WEMO_DOMAIN, self.unique_id)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } + + @property + def is_on(self) -> bool: + """Return true if the state is on. Standby is on.""" + return self._state + + async def async_added_to_hass(self) -> None: + """Wemo device added to Home Assistant.""" + await super().async_added_to_hass() + + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.register, self.wemo) + registry.on(self.wemo, None, self._subscription_callback) + + async def async_will_remove_from_hass(self) -> None: + """Wemo device removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + + def _subscription_callback( + self, _device: WeMoDevice, _type: str, _params: str + ) -> None: + """Update the state by the Wemo device.""" + _LOGGER.info("Subscription update for %s", self.name) + updated = self.wemo.subscription_update(_type, _params) + self.hass.add_job(self._async_locked_subscription_callback(not updated)) + + async def _async_locked_subscription_callback(self, force_update: bool) -> None: + """Handle an update from a subscription.""" + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + await self._async_locked_update(force_update) + self.async_write_ha_state() diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index c7325f776f..0d5ded7b82 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol @@ -24,6 +23,7 @@ from .const import ( SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY, ) +from .entity import WemoSubscriptionEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -143,15 +143,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoHumidifier(FanEntity): +class WemoHumidifier(WemoSubscriptionEntity, FanEntity): """Representation of a WeMo humidifier.""" def __init__(self, device): """Initialize the WeMo switch.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None + super().__init__(device) self._fan_mode = None self._target_humidity = None self._current_humidity = None @@ -159,54 +156,6 @@ class WemoHumidifier(FanEntity): self._filter_life = None self._filter_expired = None self._last_fan_on_mode = WEMO_FAN_MEDIUM - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo humidifier.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the humidifier if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def icon(self): @@ -240,44 +189,6 @@ class WemoHumidifier(FanEntity): """Flag supported features.""" return SUPPORTED_FEATURES - async def async_added_to_hass(self): - """Wemo humidifier added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo humidifier removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo humidifier is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - def _update(self, force_update=True): """Update the device state.""" try: diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 4c88c157f9..1362c7d483 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant import util @@ -22,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoEntity, WemoSubscriptionEntity MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -81,21 +81,17 @@ def setup_bridge(hass, bridge, async_add_entities): update_lights() -class WemoLight(LightEntity): +class WemoLight(WemoEntity, LightEntity): """Representation of a WeMo light.""" def __init__(self, device, update_lights): """Initialize the WeMo light.""" - self.wemo = device - self._state = None + super().__init__(device) self._update_lights = update_lights - self._available = True - self._update_lock = None self._brightness = None self._hs_color = None self._color_temp = None self._is_on = None - self._name = self.wemo.name self._unique_id = self.wemo.uniqueID self._model_name = type(self.wemo).__name__ @@ -107,18 +103,13 @@ class WemoLight(LightEntity): @property def unique_id(self): """Return the ID of this light.""" - return self._unique_id - - @property - def name(self): - """Return the name of the light.""" - return self._name + return self.wemo.uniqueID @property def device_info(self): """Return the device info.""" return { - "name": self._name, + "name": self.name, "identifiers": {(WEMO_DOMAIN, self._unique_id)}, "model": self._model_name, "manufacturer": "Belkin", @@ -149,11 +140,6 @@ class WemoLight(LightEntity): """Flag supported features.""" return SUPPORT_WEMO - @property - def available(self): - """Return if light is available.""" - return self._available - def turn_on(self, **kwargs): """Turn the light on.""" xy_color = None @@ -222,111 +208,14 @@ class WemoLight(LightEntity): else: self._hs_color = None - async def async_update(self): - """Synchronize state with bridge.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - -class WemoDimmer(LightEntity): +class WemoDimmer(WemoSubscriptionEntity, LightEntity): """Representation of a WeMo dimmer.""" def __init__(self, device): """Initialize the WeMo dimmer.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None + super().__init__(device) self._brightness = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Wemo dimmer added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo dimmer removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo dimmer is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - @property - def unique_id(self): - """Return the ID of this WeMo dimmer.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the dimmer if any.""" - return self._name - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def supported_features(self): @@ -338,11 +227,6 @@ class WemoDimmer(LightEntity): """Return the brightness of this light between 1 and 100.""" return self._brightness - @property - def is_on(self): - """Return true if dimmer is on. Standby is on.""" - return self._state - def _update(self, force_update=True): """Update the device state.""" try: @@ -390,8 +274,3 @@ class WemoDimmer(LightEntity): self._available = False self.schedule_update_ha_state() - - @property - def available(self): - """Return if dimmer is available.""" - return self._available diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index e2210d0279..50926e07a1 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -3,7 +3,6 @@ import asyncio from datetime import datetime, timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.switch import SwitchEntity @@ -12,6 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoSubscriptionEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -49,57 +49,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoSwitch(SwitchEntity): +class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): """Representation of a WeMo switch.""" def __init__(self, device): """Initialize the WeMo switch.""" - self.wemo = device + super().__init__(device) self.insight_params = None self.maker_params = None self.coffeemaker_mode = None - self._state = None self._mode_string = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo switch.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def device_state_attributes(self): @@ -172,20 +131,10 @@ class WemoSwitch(SwitchEntity): return STATE_STANDBY return STATE_UNKNOWN - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - @property def icon(self): """Return the icon of device based on its type.""" - if self._model_name == "CoffeeMaker": + if self.wemo.model_name == "CoffeeMaker": return "mdi:coffee" return None @@ -211,55 +160,17 @@ class WemoSwitch(SwitchEntity): self.schedule_update_ha_state() - async def async_added_to_hass(self): - """Wemo switch added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo switch removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo switch is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - def _update(self, force_update): + def _update(self, force_update=True): """Update the device state.""" try: self._state = self.wemo.get_state(force_update) - if self._model_name == "Insight": + if self.wemo.model_name == "Insight": self.insight_params = self.wemo.insight_params self.insight_params["standby_state"] = self.wemo.get_standby_state - elif self._model_name == "Maker": + elif self.wemo.model_name == "Maker": self.maker_params = self.wemo.maker_params - elif self._model_name == "CoffeeMaker": + elif self.wemo.model_name == "CoffeeMaker": self.coffeemaker_mode = self.wemo.mode self._mode_string = self.wemo.mode_string From 8979c4987fd07fbd562fc0a1f49353b5e6a0339a Mon Sep 17 00:00:00 2001 From: Crash Date: Sun, 13 Dec 2020 03:10:51 -0800 Subject: [PATCH 108/302] Clear mpd source playlist when not playing a playlist (#44164) Prevents unexpected behavior. --- homeassistant/components/mpd/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 69ab0a3421..280956c7dd 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -365,6 +365,7 @@ class MpdDevice(MediaPlayerEntity): self._client.play() else: self._client.clear() + self._currentplaylist = None self._client.add(media_id) self._client.play() From 485cd06de9a8f5efa79a95e34178932caf2c7c77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Dec 2020 05:36:17 -0600 Subject: [PATCH 109/302] Bump zeroconf to 0.28.7 to fix thread safety (#44160) Service registration was not thread safe --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1848c89057..753ac2a244 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.6"], + "requirements": ["zeroconf==0.28.7"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 180305e8ed..7cdec1fd7b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.4.2 -zeroconf==0.28.6 +zeroconf==0.28.7 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 855cfbc311..3c3d5dbe64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,7 +2342,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.6 +zeroconf==0.28.7 # homeassistant.components.zha zha-quirks==0.0.48 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7da703b01d..52663d3f89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ yeelight==0.5.4 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.28.6 +zeroconf==0.28.7 # homeassistant.components.zha zha-quirks==0.0.48 From 6fb6d771fd67bd5b2e66fe0876db681d488a3b63 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 13 Dec 2020 08:43:33 -0500 Subject: [PATCH 110/302] Bump the ZHA quirks lib to 0.0.49 (#44173) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7a36348b72..ed2460614f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.21.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.48", + "zha-quirks==0.0.49", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", "zigpy==0.28.2", diff --git a/requirements_all.txt b/requirements_all.txt index 3c3d5dbe64..c9299bab07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2345,7 +2345,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.48 +zha-quirks==0.0.49 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52663d3f89..0b8b09ef17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1147,7 +1147,7 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.48 +zha-quirks==0.0.49 # homeassistant.components.zha zigpy-cc==0.5.2 From 350ffd3e8514649f15f43a3ad7fa01dc508fd324 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Dec 2020 13:00:53 -0600 Subject: [PATCH 111/302] Bump HAP-python to 3.1.0 (#44176) Fixes many spec compliance issues, unavailable cases following an unexpected exception, and a thread safety race condition. --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 486e9f1643..d188dd270a 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.0.0", + "HAP-python==3.1.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index c9299bab07..b711bb9528 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.0.0 +HAP-python==3.1.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b8b09ef17..a56ab2d42b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==3.0.0 +HAP-python==3.1.0 # homeassistant.components.flick_electric PyFlick==0.0.2 From ecf9aac1cefbb5b455081580777ea671ebd12bc6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 14 Dec 2020 00:03:52 +0000 Subject: [PATCH 112/302] [ci skip] Translation update --- homeassistant/components/enocean/translations/no.json | 2 +- homeassistant/components/kulersky/translations/hu.json | 9 +++++++++ homeassistant/components/nws/translations/no.json | 2 +- homeassistant/components/nws/translations/zh-Hant.json | 2 +- homeassistant/components/rfxtrx/translations/no.json | 2 +- homeassistant/components/tuya/translations/hu.json | 3 +++ homeassistant/components/upb/translations/no.json | 2 +- 7 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/kulersky/translations/hu.json diff --git a/homeassistant/components/enocean/translations/no.json b/homeassistant/components/enocean/translations/no.json index 775443d8f5..eefa1fd2dd 100644 --- a/homeassistant/components/enocean/translations/no.json +++ b/homeassistant/components/enocean/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_dongle_path": "Ugyldig donglesti", + "invalid_dongle_path": "Ugyldig donglebane", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/kulersky/translations/hu.json b/homeassistant/components/kulersky/translations/hu.json new file mode 100644 index 0000000000..3d5be90042 --- /dev/null +++ b/homeassistant/components/kulersky/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "El akarod kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/no.json b/homeassistant/components/nws/translations/no.json index d9a17545c4..556e9b4136 100644 --- a/homeassistant/components/nws/translations/no.json +++ b/homeassistant/components/nws/translations/no.json @@ -15,7 +15,7 @@ "longitude": "Lengdegrad", "station": "METAR stasjonskode" }, - "description": "Hvis en METAR-stasjonskode ikke er spesifisert, vil breddegrad og lengdegrad brukes til \u00e5 finne den n\u00e6rmeste stasjonen.", + "description": "Hvis en METAR-stasjonskode ikke er spesifisert, vil breddegrad og lengdegrad bli brukt til \u00e5 finne n\u00e6rmeste stasjon. For n\u00e5 kan en API-n\u00f8kkel v\u00e6re hva som helst. Det anbefales \u00e5 bruke en gyldig e-postadresse.", "title": "Koble til National Weather Service" } } diff --git a/homeassistant/components/nws/translations/zh-Hant.json b/homeassistant/components/nws/translations/zh-Hant.json index 067234c7b5..c3abf6ceba 100644 --- a/homeassistant/components/nws/translations/zh-Hant.json +++ b/homeassistant/components/nws/translations/zh-Hant.json @@ -15,7 +15,7 @@ "longitude": "\u7d93\u5ea6", "station": "METAR \u6a5f\u5834\u4ee3\u78bc" }, - "description": "\u5047\u5982\u672a\u6307\u5b9a METAR \u6a5f\u5834\u4ee3\u78bc\uff0c\u5c07\u6703\u4f7f\u7528\u7d93\u7def\u5ea6\u8cc7\u8a0a\u5c0b\u627e\u6700\u8fd1\u7684\u6a5f\u5834\u3002", + "description": "\u5047\u5982\u672a\u6307\u5b9a METAR \u6a5f\u5834\u4ee3\u78bc\uff0c\u5c07\u6703\u4f7f\u7528\u7d93\u7def\u5ea6\u8cc7\u8a0a\u5c0b\u627e\u6700\u8fd1\u7684\u6a5f\u5834\u3002\u76ee\u524d\uff0cAPI \u5bc6\u9470\u53ef\u8f38\u5165\u4efb\u4f55\u8cc7\u8a0a\uff0c\u5efa\u8b70\u70ba\u6709\u6548\u5730\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002", "title": "\u9023\u7dda\u81f3\u7f8e\u570b\u570b\u5bb6\u6c23\u8c61\u5c40\u670d\u52d9" } } diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 3323384072..752136dac7 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -25,7 +25,7 @@ "data": { "device": "USB enhetsbane" }, - "title": "Sti" + "title": "Bane" }, "user": { "data": { diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 7c90b93732..b128be6708 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "A kapcsol\u00f3d\u00e1s nem siker\u00fclt" + }, "error": { "dev_multi_type": "A konfigur\u00e1land\u00f3 eszk\u00f6z\u00f6knek azonos t\u00edpus\u00faaknak kell lennie", "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3", diff --git a/homeassistant/components/upb/translations/no.json b/homeassistant/components/upb/translations/no.json index 34295b718c..e280388eec 100644 --- a/homeassistant/components/upb/translations/no.json +++ b/homeassistant/components/upb/translations/no.json @@ -12,7 +12,7 @@ "user": { "data": { "address": "Adresse (se beskrivelse over)", - "file_path": "Sti og navn p\u00e5 UPStart UPB-eksportfilen.", + "file_path": "Bane og navn p\u00e5 UPStart UPB-eksportfilen.", "protocol": "Protokoll" }, "description": "Koble til en universal Powerline Bus Powerline Interface Module (UPB PIM). Adressestrengen m\u00e5 v\u00e6re i skjemaet 'adresse[:port]' for 'tcp'. Porten er valgfri og bruker som standard til 2101. Eksempel: '192.168.1.42'. For serieprotokollen m\u00e5 adressen v\u00e6re i skjemaet 'tty[:baud]'. Baud er valgfritt og standard til 4800. Eksempel: '/dev/ttyS1'.", From 66ed34e60ef8028c9288168d2b7f4f0ae4b75dee Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 14 Dec 2020 10:04:41 +0100 Subject: [PATCH 113/302] Update denonavr to 0.9.8 (#44194) --- 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 c8341a3ec2..31085292fb 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.7", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.8", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index b711bb9528..45122f548c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -481,7 +481,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.7 +denonavr==0.9.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a56ab2d42b..fd03379d22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.7 +denonavr==0.9.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 From b758c147a6476fb7adb07d158b79b498970001cb Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Mon, 14 Dec 2020 09:05:15 +0000 Subject: [PATCH 114/302] Add myself to the codeowners manifest for openhome and tapsaff (#44188) --- CODEOWNERS | 2 ++ homeassistant/components/openhome/manifest.json | 2 +- homeassistant/components/tapsaff/manifest.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 27614c3d49..8e2dd1fa56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -323,6 +323,7 @@ homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm homeassistant/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen +homeassistant/components/openhome/* @bazwilliams homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff @freekode @nzapponi @@ -453,6 +454,7 @@ homeassistant/components/tado/* @michaelarnauts @bdraco homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages +homeassistant/components/tapsaff/* @bazwilliams homeassistant/components/tasmota/* @emontnemery homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 3a94215a7b..98fbf2d961 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -3,5 +3,5 @@ "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==0.7.2"], - "codeowners": [] + "codeowners": ["@bazwilliams"] } diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 7d78491ad1..30b9a2066c 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -3,5 +3,5 @@ "name": "Taps Aff", "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": ["tapsaff==0.2.1"], - "codeowners": [] + "codeowners": ["@bazwilliams"] } From dc1d08be7082f42d70a0adee35143730f77d50a6 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 14 Dec 2020 11:06:21 +0200 Subject: [PATCH 115/302] Upgrade restrictedpython to 5.1 (needed for python 3.9 support) (#44181) --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index fad88a19b3..0b5af0ae43 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.0"], + "requirements": ["restrictedpython==5.1"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 45122f548c..c1c78b425a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1931,7 +1931,7 @@ raspyrfm-client==1.2.8 regenmaschine==3.0.0 # homeassistant.components.python_script -restrictedpython==5.0 +restrictedpython==5.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd03379d22..c6c65f5226 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -948,7 +948,7 @@ rachiopy==1.0.3 regenmaschine==3.0.0 # homeassistant.components.python_script -restrictedpython==5.0 +restrictedpython==5.1 # homeassistant.components.rflink rflink==0.0.55 From 61dd374713497d6ff1040143d1ee2bf34b0265c4 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 14 Dec 2020 10:08:28 +0100 Subject: [PATCH 116/302] Upgrade youtube_dl to version 2020.12.07 (#44004) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index b834cbc0aa..3c07fbc4e2 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.11.12"], + "requirements": ["youtube_dl==2020.12.07"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index c1c78b425a..1fc3ba1d96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2333,7 +2333,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.11.12 +youtube_dl==2020.12.07 # homeassistant.components.onvif zeep[async]==4.0.0 From c27c958a4d853eb181faa0c042438d476ba1a86c Mon Sep 17 00:00:00 2001 From: Guillaume Duveau Date: Mon, 14 Dec 2020 10:46:44 +0100 Subject: [PATCH 117/302] Temperatures, fan and battery in Glances sensors (#43500) * Temperatures, fan and battery in Glances sensors * Lint PR #43500 --- homeassistant/components/glances/const.py | 4 ++- homeassistant/components/glances/sensor.py | 37 ++++++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 491d400411..69e4ce0c01 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -36,7 +36,9 @@ SENSOR_TYPES = { "process_thread": ["processcount", "Thread", "Count", CPU_ICON], "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON], "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON], - "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_core": ["sensors", "temperature", TEMP_CELSIUS, "mdi:thermometer"], + "fan_speed": ["sensors", "fan speed", "RPM", "mdi:fan"], + "battery": ["sensors", "charge", PERCENTAGE, "mdi:battery"], "docker_active": ["docker", "Containers active", "", "mdi:docker"], "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker"], "docker_memory_use": [ diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index fb36312cf1..4c534a90ae 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -34,16 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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], + if sensor["type"] == sensor_type: + 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( @@ -156,11 +157,21 @@ class GlancesSensor(Entity): (disk["size"] - disk["used"]) / 1024 ** 3, 1, ) - elif self.type == "sensor_temp": + elif self.type == "battery": for sensor in value["sensors"]: - if sensor["label"] == self._sensor_name_prefix: - self._state = sensor["value"] - break + if sensor["type"] == "battery": + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] + elif self.type == "fan_speed": + for sensor in value["sensors"]: + if sensor["type"] == "fan_speed": + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] + elif self.type == "temperature_core": + for sensor in value["sensors"]: + if sensor["type"] == "temperature_core": + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] elif self.type == "memory_use_percent": self._state = value["mem"]["percent"] elif self.type == "memory_use": From 7fa26ef51551e0ab84a9ca3cf0c10d32be10dee2 Mon Sep 17 00:00:00 2001 From: Steve Brandt Date: Mon, 14 Dec 2020 10:50:19 +0100 Subject: [PATCH 118/302] Add opensky longitude and latitude event metadata (#43205) * Adds feature to get also longitude and latitude of the triggerd entry or exit event * None as initial definition of longitude and latitude if it is not defined in the metadata --- homeassistant/components/opensky/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 49edf8e7d0..06132e83e8 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -117,14 +117,20 @@ class OpenSkySensor(Entity): for flight in flights: if flight in metadata: altitude = metadata[flight].get(ATTR_ALTITUDE) + longitude = metadata[flight].get(ATTR_LONGITUDE) + latitude = metadata[flight].get(ATTR_LATITUDE) else: # Assume Flight has landed if missing. altitude = 0 + longitude = None + latitude = None data = { ATTR_CALLSIGN: flight, ATTR_ALTITUDE: altitude, ATTR_SENSOR: self._name, + ATTR_LONGITUDE: longitude, + ATTR_LATITUDE: latitude, } self._hass.bus.fire(event, data) From c3d8b1323cad863221731f36f226c92d8f44cfd8 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 14 Dec 2020 09:57:22 +0000 Subject: [PATCH 119/302] Support MSSQL in SQL Sensor (#42778) * add mssql support * add tests and odbc dependency * fix requirements * no pyodbc dependency --- homeassistant/components/sql/sensor.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 27656c260d..670f5e6614 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -74,6 +74,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if value_template is not None: value_template.hass = hass + # MSSQL uses TOP and not LIMIT + if not ("LIMIT" in query_str or "SELECT TOP" in query_str): + query_str = ( + query_str.replace("SELECT", "SELECT TOP 1") + if "mssql" in db_url + else query_str.replace(";", " LIMIT 1;") + ) + sensor = SQLSensor( name, sessmaker, query_str, column_name, unit, value_template ) @@ -88,10 +96,7 @@ class SQLSensor(Entity): def __init__(self, name, sessmaker, query, column, unit, value_template): """Initialize the SQL sensor.""" self._name = name - if "LIMIT" in query: - self._query = query - else: - self._query = query.replace(";", " LIMIT 1;") + self._query = query self._unit_of_measurement = unit self._template = value_template self._column_name = column From a7fca3cf23313a61cda4ada2ef20176092b17d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mayoral=20Mart=C3=ADnez?= Date: Mon, 14 Dec 2020 15:16:39 +0100 Subject: [PATCH 120/302] Bump python-holidays (#44215) --- 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 70d6605320..4fb25c766c 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.10.3"], + "requirements": ["holidays==0.10.4"], "codeowners": ["@fabaff"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 1fc3ba1d96..50469e8951 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.3 +holidays==0.10.4 # homeassistant.components.frontend home-assistant-frontend==20201212.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6c65f5226..6f7316d8a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -391,7 +391,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.3 +holidays==0.10.4 # homeassistant.components.frontend home-assistant-frontend==20201212.0 From 38d16d3e0c04421d780bd9cfc1d7d182d8f34c59 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 14 Dec 2020 13:03:25 -0700 Subject: [PATCH 121/302] Fix unhandled KeyError in Recollect Waste (#44224) --- homeassistant/components/recollect_waste/manifest.json | 2 +- homeassistant/components/recollect_waste/sensor.py | 6 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 4e6b71d59b..dc8a85ce2a 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ - "aiorecollect==0.2.2" + "aiorecollect==1.0.1" ], "codeowners": [ "@bachya" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 7ce75b1e3f..53304c9321 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -120,9 +120,11 @@ class RecollectWasteSensor(CoordinatorEntity): self._state = pickup_event.date self._attributes.update( { - ATTR_PICKUP_TYPES: pickup_event.pickup_types, + ATTR_PICKUP_TYPES: [t.name for t in pickup_event.pickup_types], ATTR_AREA_NAME: pickup_event.area_name, - ATTR_NEXT_PICKUP_TYPES: next_pickup_event.pickup_types, + ATTR_NEXT_PICKUP_TYPES: [ + t.name for t in next_pickup_event.pickup_types + ], ATTR_NEXT_PICKUP_DATE: next_date, } ) diff --git a/requirements_all.txt b/requirements_all.txt index 50469e8951..d8ba24aa21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -215,7 +215,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.recollect_waste -aiorecollect==0.2.2 +aiorecollect==1.0.1 # homeassistant.components.shelly aioshelly==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f7316d8a4..8dc7f9e1fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.recollect_waste -aiorecollect==0.2.2 +aiorecollect==1.0.1 # homeassistant.components.shelly aioshelly==0.5.1 From 0b8529a47219378d47621696870fe44124e382b7 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Mon, 14 Dec 2020 23:32:45 +0300 Subject: [PATCH 122/302] Add zha AnalogOutput cluster support (#44092) * Initial commit * black * isort * Commit suggestion from code review Co-authored-by: Alexei Chetroi * pylint * removed entity cache for present_value * fix discovery * move write_attributes to channel * write_attributes fix * write_attributes yet another fix * update_before_add=False * mains powered test device * removed test_restore_state * flake8 * removed async_configure_channel_specific * don't know what this patch does, removing * test for async_update * removed node_descriptor * fix unit_of_measurement Co-authored-by: Alexei Chetroi --- .../components/zha/core/channels/general.py | 75 ++++ homeassistant/components/zha/core/const.py | 3 + .../components/zha/core/discovery.py | 1 + .../components/zha/core/registries.py | 2 + homeassistant/components/zha/number.py | 339 ++++++++++++++++++ tests/components/zha/test_number.py | 130 +++++++ tests/components/zha/zha_devices_list.py | 12 + 7 files changed, 562 insertions(+) create mode 100644 homeassistant/components/zha/number.py create mode 100644 tests/components/zha/test_number.py diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index fa4883ae5a..d105572c18 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -4,6 +4,7 @@ from typing import Any, Coroutine, List, Optional import zigpy.exceptions import zigpy.zcl.clusters.general as general +from zigpy.zcl.foundation import Status from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -19,6 +20,7 @@ from ..const import ( SIGNAL_SET_LEVEL, SIGNAL_UPDATE_DEVICE, ) +from ..helpers import retryable_req from .base import ClientChannel, ZigbeeChannel, parse_and_log_command @@ -34,12 +36,85 @@ class AnalogInput(ZigbeeChannel): REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] +@registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) class AnalogOutput(ZigbeeChannel): """Analog Output channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + @property + def present_value(self) -> Optional[float]: + """Return cached value of present_value.""" + return self.cluster.get("present_value") + + @property + def min_present_value(self) -> Optional[float]: + """Return cached value of min_present_value.""" + return self.cluster.get("min_present_value") + + @property + def max_present_value(self) -> Optional[float]: + """Return cached value of max_present_value.""" + return self.cluster.get("max_present_value") + + @property + def resolution(self) -> Optional[float]: + """Return cached value of resolution.""" + return self.cluster.get("resolution") + + @property + def relinquish_default(self) -> Optional[float]: + """Return cached value of relinquish_default.""" + return self.cluster.get("relinquish_default") + + @property + def description(self) -> Optional[str]: + """Return cached value of description.""" + return self.cluster.get("description") + + @property + def engineering_units(self) -> Optional[int]: + """Return cached value of engineering_units.""" + return self.cluster.get("engineering_units") + + @property + def application_type(self) -> Optional[int]: + """Return cached value of application_type.""" + return self.cluster.get("application_type") + + async def async_set_present_value(self, value: float) -> bool: + """Update present_value.""" + try: + res = await self.cluster.write_attributes({"present_value": value}) + except zigpy.exceptions.ZigbeeException as ex: + self.error("Could not set value: %s", ex) + return False + if isinstance(res, list) and all( + [record.status == Status.SUCCESS for record in res[0]] + ): + return True + return False + + @retryable_req(delays=(1, 1, 3)) + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: + """Initialize channel.""" + return self.fetch_config(from_cache) + + async def fetch_config(self, from_cache: bool) -> None: + """Get the channel configuration.""" + attributes = [ + "min_present_value", + "max_present_value", + "resolution", + "relinquish_default", + "description", + "engineering_units", + "application_type", + ] + # just populates the cache, if not already done + await self.get_attributes(attributes, from_cache=from_cache) + @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) class AnalogValue(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 12d928e172..1d3f767353 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK +from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH @@ -71,6 +72,7 @@ BINDINGS = "bindings" CHANNEL_ACCELEROMETER = "accelerometer" CHANNEL_ANALOG_INPUT = "analog_input" +CHANNEL_ANALOG_OUTPUT = "analog_output" CHANNEL_ATTRIBUTE = "attribute" CHANNEL_BASIC = "basic" CHANNEL_COLOR = "light_color" @@ -110,6 +112,7 @@ COMPONENTS = ( FAN, LIGHT, LOCK, + NUMBER, SENSOR, SWITCH, ) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 05a12bc228..e071a52332 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -22,6 +22,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, fan, light, lock, + number, sensor, switch, ) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index e2b4056cfa..4dcccc98c0 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -14,6 +14,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK +from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH @@ -61,6 +62,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.closures.DoorLock.cluster_id: LOCK, zcl.clusters.closures.WindowCovering.cluster_id: COVER, zcl.clusters.general.AnalogInput.cluster_id: SENSOR, + zcl.clusters.general.AnalogOutput.cluster_id: NUMBER, zcl.clusters.general.MultistateInput.cluster_id: SENSOR, zcl.clusters.general.OnOff.cluster_id: SWITCH, zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR, diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py new file mode 100644 index 0000000000..b4772e5174 --- /dev/null +++ b/homeassistant/components/zha/number.py @@ -0,0 +1,339 @@ +"""Support for ZHA AnalogOutput cluster.""" +import functools +import logging + +from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .core import discovery +from .core.const import ( + CHANNEL_ANALOG_OUTPUT, + DATA_ZHA, + DATA_ZHA_DISPATCHERS, + SIGNAL_ADD_ENTITIES, + SIGNAL_ATTR_UPDATED, +) +from .core.registries import ZHA_ENTITIES +from .entity import ZhaEntity + +_LOGGER = logging.getLogger(__name__) + +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) + + +UNITS = { + 0: "Square-meters", + 1: "Square-feet", + 2: "Milliamperes", + 3: "Amperes", + 4: "Ohms", + 5: "Volts", + 6: "Kilo-volts", + 7: "Mega-volts", + 8: "Volt-amperes", + 9: "Kilo-volt-amperes", + 10: "Mega-volt-amperes", + 11: "Volt-amperes-reactive", + 12: "Kilo-volt-amperes-reactive", + 13: "Mega-volt-amperes-reactive", + 14: "Degrees-phase", + 15: "Power-factor", + 16: "Joules", + 17: "Kilojoules", + 18: "Watt-hours", + 19: "Kilowatt-hours", + 20: "BTUs", + 21: "Therms", + 22: "Ton-hours", + 23: "Joules-per-kilogram-dry-air", + 24: "BTUs-per-pound-dry-air", + 25: "Cycles-per-hour", + 26: "Cycles-per-minute", + 27: "Hertz", + 28: "Grams-of-water-per-kilogram-dry-air", + 29: "Percent-relative-humidity", + 30: "Millimeters", + 31: "Meters", + 32: "Inches", + 33: "Feet", + 34: "Watts-per-square-foot", + 35: "Watts-per-square-meter", + 36: "Lumens", + 37: "Luxes", + 38: "Foot-candles", + 39: "Kilograms", + 40: "Pounds-mass", + 41: "Tons", + 42: "Kilograms-per-second", + 43: "Kilograms-per-minute", + 44: "Kilograms-per-hour", + 45: "Pounds-mass-per-minute", + 46: "Pounds-mass-per-hour", + 47: "Watts", + 48: "Kilowatts", + 49: "Megawatts", + 50: "BTUs-per-hour", + 51: "Horsepower", + 52: "Tons-refrigeration", + 53: "Pascals", + 54: "Kilopascals", + 55: "Bars", + 56: "Pounds-force-per-square-inch", + 57: "Centimeters-of-water", + 58: "Inches-of-water", + 59: "Millimeters-of-mercury", + 60: "Centimeters-of-mercury", + 61: "Inches-of-mercury", + 62: "°C", + 63: "°K", + 64: "°F", + 65: "Degree-days-Celsius", + 66: "Degree-days-Fahrenheit", + 67: "Years", + 68: "Months", + 69: "Weeks", + 70: "Days", + 71: "Hours", + 72: "Minutes", + 73: "Seconds", + 74: "Meters-per-second", + 75: "Kilometers-per-hour", + 76: "Feet-per-second", + 77: "Feet-per-minute", + 78: "Miles-per-hour", + 79: "Cubic-feet", + 80: "Cubic-meters", + 81: "Imperial-gallons", + 82: "Liters", + 83: "Us-gallons", + 84: "Cubic-feet-per-minute", + 85: "Cubic-meters-per-second", + 86: "Imperial-gallons-per-minute", + 87: "Liters-per-second", + 88: "Liters-per-minute", + 89: "Us-gallons-per-minute", + 90: "Degrees-angular", + 91: "Degrees-Celsius-per-hour", + 92: "Degrees-Celsius-per-minute", + 93: "Degrees-Fahrenheit-per-hour", + 94: "Degrees-Fahrenheit-per-minute", + 95: None, + 96: "Parts-per-million", + 97: "Parts-per-billion", + 98: "%", + 99: "Percent-per-second", + 100: "Per-minute", + 101: "Per-second", + 102: "Psi-per-Degree-Fahrenheit", + 103: "Radians", + 104: "Revolutions-per-minute", + 105: "Currency1", + 106: "Currency2", + 107: "Currency3", + 108: "Currency4", + 109: "Currency5", + 110: "Currency6", + 111: "Currency7", + 112: "Currency8", + 113: "Currency9", + 114: "Currency10", + 115: "Square-inches", + 116: "Square-centimeters", + 117: "BTUs-per-pound", + 118: "Centimeters", + 119: "Pounds-mass-per-second", + 120: "Delta-Degrees-Fahrenheit", + 121: "Delta-Degrees-Kelvin", + 122: "Kilohms", + 123: "Megohms", + 124: "Millivolts", + 125: "Kilojoules-per-kilogram", + 126: "Megajoules", + 127: "Joules-per-degree-Kelvin", + 128: "Joules-per-kilogram-degree-Kelvin", + 129: "Kilohertz", + 130: "Megahertz", + 131: "Per-hour", + 132: "Milliwatts", + 133: "Hectopascals", + 134: "Millibars", + 135: "Cubic-meters-per-hour", + 136: "Liters-per-hour", + 137: "Kilowatt-hours-per-square-meter", + 138: "Kilowatt-hours-per-square-foot", + 139: "Megajoules-per-square-meter", + 140: "Megajoules-per-square-foot", + 141: "Watts-per-square-meter-Degree-Kelvin", + 142: "Cubic-feet-per-second", + 143: "Percent-obscuration-per-foot", + 144: "Percent-obscuration-per-meter", + 145: "Milliohms", + 146: "Megawatt-hours", + 147: "Kilo-BTUs", + 148: "Mega-BTUs", + 149: "Kilojoules-per-kilogram-dry-air", + 150: "Megajoules-per-kilogram-dry-air", + 151: "Kilojoules-per-degree-Kelvin", + 152: "Megajoules-per-degree-Kelvin", + 153: "Newton", + 154: "Grams-per-second", + 155: "Grams-per-minute", + 156: "Tons-per-hour", + 157: "Kilo-BTUs-per-hour", + 158: "Hundredths-seconds", + 159: "Milliseconds", + 160: "Newton-meters", + 161: "Millimeters-per-second", + 162: "Millimeters-per-minute", + 163: "Meters-per-minute", + 164: "Meters-per-hour", + 165: "Cubic-meters-per-minute", + 166: "Meters-per-second-per-second", + 167: "Amperes-per-meter", + 168: "Amperes-per-square-meter", + 169: "Ampere-square-meters", + 170: "Farads", + 171: "Henrys", + 172: "Ohm-meters", + 173: "Siemens", + 174: "Siemens-per-meter", + 175: "Teslas", + 176: "Volts-per-degree-Kelvin", + 177: "Volts-per-meter", + 178: "Webers", + 179: "Candelas", + 180: "Candelas-per-square-meter", + 181: "Kelvins-per-hour", + 182: "Kelvins-per-minute", + 183: "Joule-seconds", + 185: "Square-meters-per-Newton", + 186: "Kilogram-per-cubic-meter", + 187: "Newton-seconds", + 188: "Newtons-per-meter", + 189: "Watts-per-meter-per-degree-Kelvin", +} + +ICONS = { + 0: "mdi:temperature-celsius", + 1: "mdi:water-percent", + 2: "mdi:gauge", + 3: "mdi:speedometer", + 4: "mdi:percent", + 5: "mdi:air-filter", + 6: "mdi:fan", + 7: "mdi:flash", + 8: "mdi:current-ac", + 9: "mdi:flash", + 10: "mdi:flash", + 11: "mdi:flash", + 12: "mdi:counter", + 13: "mdi:thermometer-lines", + 14: "mdi:timer", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation Analog Output from config entry.""" + entities_to_create = hass.data[DATA_ZHA][DOMAIN] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, + ), + ) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + +@STRICT_MATCH(channel_names=CHANNEL_ANALOG_OUTPUT) +class ZhaNumber(ZhaEntity, NumberEntity): + """Representation of a ZHA Number entity.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this entity.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._analog_output_channel = self.cluster_channels.get(CHANNEL_ANALOG_OUTPUT) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + self.async_accept_signal( + self._analog_output_channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + + @property + def value(self): + """Return the current value.""" + return self._analog_output_channel.present_value + + @property + def min_value(self): + """Return the minimum value.""" + min_present_value = self._analog_output_channel.min_present_value + if min_present_value is not None: + return min_present_value + return 0 + + @property + def max_value(self): + """Return the maximum value.""" + max_present_value = self._analog_output_channel.max_present_value + if max_present_value is not None: + return max_present_value + return 1023 + + @property + def step(self): + """Return the value step.""" + resolution = self._analog_output_channel.resolution + if resolution is not None: + return resolution + return super().step + + @property + def name(self): + """Return the name of the number entity.""" + description = self._analog_output_channel.description + if description is not None and len(description) > 0: + return f"{super().name} {description}" + return super().name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + application_type = self._analog_output_channel.application_type + if application_type is not None: + return ICONS.get(application_type >> 16, super().icon) + return super().icon + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + engineering_units = self._analog_output_channel.engineering_units + return UNITS.get(engineering_units) + + @callback + def async_set_state(self, attr_id, attr_name, value): + """Handle value update from channel.""" + self.async_write_ha_state() + + async def async_set_value(self, value): + """Update the current value from HA.""" + num_value = float(value) + if await self._analog_output_channel.async_set_present_value(num_value): + self.async_write_ha_state() + + async def async_update(self): + """Attempt to retrieve the state of the entity.""" + await super().async_update() + _LOGGER.debug("polling current state") + if self._analog_output_channel: + value = await self._analog_output_channel.get_attribute_value( + "present_value", from_cache=False + ) + _LOGGER.debug("read value=%s", value) diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py new file mode 100644 index 0000000000..947bad37e0 --- /dev/null +++ b/tests/components/zha/test_number.py @@ -0,0 +1,130 @@ +"""Test zha analog output.""" +import pytest +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.number import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component + +from .common import ( + async_enable_traffic, + async_test_rejoin, + find_entity_id, + send_attributes_report, +) + +from tests.async_mock import call, patch +from tests.common import mock_coro + + +@pytest.fixture +def zigpy_analog_output_device(zigpy_device_mock): + """Zigpy analog_output device.""" + + endpoints = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, + "in_clusters": [general.AnalogOutput.cluster_id, general.Basic.cluster_id], + "out_clusters": [], + } + } + return zigpy_device_mock(endpoints) + + +async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_device): + """Test zha number platform.""" + + cluster = zigpy_analog_output_device.endpoints.get(1).analog_output + cluster.PLUGGED_ATTR_READS = { + "present_value": 15.0, + "max_present_value": 100.0, + "min_present_value": 0.0, + "relinquish_default": 50.0, + "resolution": 1.0, + "description": "PWM1", + "engineering_units": 98, + "application_type": 4 * 0x10000, + } + zha_device = await zha_device_joined_restored(zigpy_analog_output_device) + # one for present_value and one for the rest configuration attributes + assert cluster.read_attributes.call_count == 2 + assert "max_present_value" in cluster.read_attributes.call_args[0][0] + assert "min_present_value" in cluster.read_attributes.call_args[0][0] + assert "relinquish_default" in cluster.read_attributes.call_args[0][0] + assert "resolution" in cluster.read_attributes.call_args[0][0] + assert "description" in cluster.read_attributes.call_args[0][0] + assert "engineering_units" in cluster.read_attributes.call_args[0][0] + assert "application_type" in cluster.read_attributes.call_args[0][0] + + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the number was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + assert cluster.read_attributes.call_count == 2 + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + assert cluster.read_attributes.call_count == 4 + + # test that the state has changed from unavailable to 15.0 + assert hass.states.get(entity_id).state == "15.0" + + # test attributes + assert hass.states.get(entity_id).attributes.get("min") == 0.0 + assert hass.states.get(entity_id).attributes.get("max") == 100.0 + assert hass.states.get(entity_id).attributes.get("step") == 1.0 + assert hass.states.get(entity_id).attributes.get("icon") == "mdi:percent" + assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" + assert ( + hass.states.get(entity_id).attributes.get("friendly_name") + == "FakeManufacturer FakeModel e769900a analog_output PWM1" + ) + + # change value from device + assert cluster.read_attributes.call_count == 4 + await send_attributes_report(hass, cluster, {0x0055: 15}) + assert hass.states.get(entity_id).state == "15.0" + + # update value from device + await send_attributes_report(hass, cluster, {0x0055: 20}) + assert hass.states.get(entity_id).state == "20.0" + + # change value from HA + with patch( + "zigpy.zcl.Cluster.write_attributes", + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), + ): + # set value via UI + await hass.services.async_call( + DOMAIN, "set_value", {"entity_id": entity_id, "value": 30.0}, blocking=True + ) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"present_value": 30.0}) + cluster.PLUGGED_ATTR_READS["present_value"] = 30.0 + + # test rejoin + assert cluster.read_attributes.call_count == 4 + await async_test_rejoin(hass, zigpy_analog_output_device, [cluster], (1,)) + assert hass.states.get(entity_id).state == "30.0" + assert cluster.read_attributes.call_count == 6 + + # update device value with failed attribute report + cluster.PLUGGED_ATTR_READS["present_value"] = 40.0 + # validate the entity still contains old value + assert hass.states.get(entity_id).state == "30.0" + + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "40.0" + assert cluster.read_attributes.call_count == 7 + assert "present_value" in cluster.read_attributes.call_args[0][0] diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 136af1f4be..1ea52d4e60 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3612,6 +3612,8 @@ DEVICES = [ "sensor.digi_xbee3_77665544_analog_input_2", "sensor.digi_xbee3_77665544_analog_input_3", "sensor.digi_xbee3_77665544_analog_input_4", + "number.digi_xbee3_77665544_analog_output", + "number.digi_xbee3_77665544_analog_output_2", ], "entity_map": { ("switch", "00:11:22:33:44:55:66:77-208-6"): { @@ -3714,6 +3716,16 @@ DEVICES = [ "entity_class": "AnalogInput", "entity_id": "sensor.digi_xbee3_77665544_analog_input_5", }, + ("number", "00:11:22:33:44:55:66:77-218-13"): { + "channels": ["analog_output"], + "entity_class": "ZhaNumber", + "entity_id": "number.digi_xbee3_77665544_analog_output", + }, + ("number", "00:11:22:33:44:55:66:77-219-13"): { + "channels": ["analog_output"], + "entity_class": "ZhaNumber", + "entity_id": "number.digi_xbee3_77665544_analog_output_2", + }, }, "event_channels": ["232:0x0008"], "manufacturer": "Digi", From d274a62c3115bd17248b0da35c11463f259da1b8 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 14 Dec 2020 13:51:28 -0800 Subject: [PATCH 123/302] Bump envoy_reader version to 0.17.3 (#44205) * Bump envoy_reader version to 0.17.0rc0 * Fixing version number --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index e6ab8dbf6a..b339013a69 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,7 +2,7 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.17.0"], + "requirements": ["envoy_reader==0.17.3"], "codeowners": [ "@gtdiehl" ] diff --git a/requirements_all.txt b/requirements_all.txt index d8ba24aa21..36fab4e430 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ env_canada==0.2.4 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.17.0 +envoy_reader==0.17.3 # homeassistant.components.season ephem==3.7.7.0 From 9e1647d63419b9cf429d347577a03c53fe6f23df Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 15 Dec 2020 00:04:49 +0000 Subject: [PATCH 124/302] [ci skip] Translation update --- .../components/airly/translations/tr.json | 7 +++ .../components/apple_tv/translations/hu.json | 18 +++++++ .../components/apple_tv/translations/tr.json | 54 +++++++++++++++++++ .../components/aurora/translations/de.json | 11 ++++ .../components/bsblan/translations/tr.json | 12 +++++ .../components/cloud/translations/it.json | 6 +-- .../components/cloud/translations/tr.json | 11 ++++ .../cloudflare/translations/tr.json | 25 +++++++++ .../device_tracker/translations/tr.json | 6 +++ .../components/dsmr/translations/tr.json | 12 +++++ .../components/epson/translations/tr.json | 13 +++++ .../components/gios/translations/it.json | 2 +- .../components/hassio/translations/it.json | 2 +- .../components/hassio/translations/tr.json | 10 ++++ .../homeassistant/translations/tr.json | 19 +++++++ .../components/hyperion/translations/tr.json | 40 ++++++++++++++ .../components/ipma/translations/tr.json | 7 +++ .../components/kulersky/translations/tr.json | 7 +++ .../components/lovelace/translations/de.json | 7 +++ .../components/lovelace/translations/tr.json | 9 ++++ .../motion_blinds/translations/tr.json | 15 ++++++ .../components/nest/translations/de.json | 5 ++ .../components/nest/translations/tr.json | 9 ++++ .../components/nws/translations/it.json | 2 +- .../ovo_energy/translations/tr.json | 14 +++++ .../components/ozw/translations/tr.json | 16 ++++++ .../components/plugwise/translations/de.json | 3 +- .../components/plugwise/translations/tr.json | 11 ++++ .../components/solaredge/translations/tr.json | 7 +++ .../components/spotify/translations/tr.json | 5 ++ .../srp_energy/translations/de.json | 7 +++ .../srp_energy/translations/tr.json | 18 +++++++ .../components/tuya/translations/tr.json | 16 ++++++ .../components/twinkly/translations/tr.json | 13 +++++ .../water_heater/translations/tr.json | 8 +++ 35 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/airly/translations/tr.json create mode 100644 homeassistant/components/apple_tv/translations/hu.json create mode 100644 homeassistant/components/apple_tv/translations/tr.json create mode 100644 homeassistant/components/aurora/translations/de.json create mode 100644 homeassistant/components/bsblan/translations/tr.json create mode 100644 homeassistant/components/cloud/translations/tr.json create mode 100644 homeassistant/components/cloudflare/translations/tr.json create mode 100644 homeassistant/components/dsmr/translations/tr.json create mode 100644 homeassistant/components/epson/translations/tr.json create mode 100644 homeassistant/components/homeassistant/translations/tr.json create mode 100644 homeassistant/components/hyperion/translations/tr.json create mode 100644 homeassistant/components/ipma/translations/tr.json create mode 100644 homeassistant/components/kulersky/translations/tr.json create mode 100644 homeassistant/components/lovelace/translations/de.json create mode 100644 homeassistant/components/lovelace/translations/tr.json create mode 100644 homeassistant/components/motion_blinds/translations/tr.json create mode 100644 homeassistant/components/nest/translations/tr.json create mode 100644 homeassistant/components/ovo_energy/translations/tr.json create mode 100644 homeassistant/components/ozw/translations/tr.json create mode 100644 homeassistant/components/plugwise/translations/tr.json create mode 100644 homeassistant/components/solaredge/translations/tr.json create mode 100644 homeassistant/components/srp_energy/translations/de.json create mode 100644 homeassistant/components/srp_energy/translations/tr.json create mode 100644 homeassistant/components/twinkly/translations/tr.json create mode 100644 homeassistant/components/water_heater/translations/tr.json diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json new file mode 100644 index 0000000000..1b6e9caa24 --- /dev/null +++ b/homeassistant/components/airly/translations/tr.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "can_reach_server": "Airly sunucusuna eri\u015fin" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json new file mode 100644 index 0000000000..4eea4abc15 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Azonos\u00edt\u00e1s nem siker\u00fclt", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "step": { + "confirm": { + "title": "Apple TV sikeresen hozz\u00e1adva" + }, + "pair_with_pin": { + "data": { + "pin": "PIN K\u00f3d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json new file mode 100644 index 0000000000..0ddc466a6f --- /dev/null +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Apple TV eklemeyi onaylay\u0131n" + }, + "pair_no_pin": { + "title": "E\u015fle\u015ftirme" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Kodu" + }, + "title": "E\u015fle\u015ftirme" + }, + "reconfigure": { + "description": "Bu Apple TV baz\u0131 ba\u011flant\u0131 sorunlar\u0131 ya\u015f\u0131yor ve yeniden yap\u0131land\u0131r\u0131lmas\u0131 gerekiyor.", + "title": "Cihaz\u0131n yeniden yap\u0131land\u0131r\u0131lmas\u0131" + }, + "service_problem": { + "title": "Hizmet eklenemedi" + }, + "user": { + "data": { + "device_input": "Cihaz" + }, + "title": "Yeni bir Apple TV kurun" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Home Assistant'\u0131 ba\u015flat\u0131rken cihaz\u0131 a\u00e7may\u0131n" + }, + "description": "Genel cihaz ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json new file mode 100644 index 0000000000..acec471c17 --- /dev/null +++ b/homeassistant/components/aurora/translations/de.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "threshold": "Schwellenwert (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json new file mode 100644 index 0000000000..94acde2d0a --- /dev/null +++ b/homeassistant/components/bsblan/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/it.json b/homeassistant/components/cloud/translations/it.json index 320ca70b81..fbe13abc41 100644 --- a/homeassistant/components/cloud/translations/it.json +++ b/homeassistant/components/cloud/translations/it.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa abilitato", - "can_reach_cert_server": "Raggiungi il server dei certificati", - "can_reach_cloud": "Raggiungi Home Assistant Cloud", - "can_reach_cloud_auth": "Raggiungi il server di autenticazione", + "can_reach_cert_server": "Server dei Certificati raggiungibile", + "can_reach_cloud": "Home Assistant Cloud raggiungibile", + "can_reach_cloud_auth": "Server di Autenticazione raggiungibile", "google_enabled": "Google abilitato", "logged_in": "Accesso effettuato", "relayer_connected": "Relayer connesso", diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json new file mode 100644 index 0000000000..0acb1e6a9a --- /dev/null +++ b/homeassistant/components/cloud/translations/tr.json @@ -0,0 +1,11 @@ +{ + "system_health": { + "info": { + "logged_in": "Giri\u015f Yapt\u0131", + "relayer_connected": "Yeniden Katman ba\u011fl\u0131", + "remote_connected": "Uzaktan Ba\u011fl\u0131", + "remote_enabled": "Uzaktan Etkinle\u015ftirildi", + "subscription_expiration": "Aboneli\u011fin Sona Ermesi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json new file mode 100644 index 0000000000..b7c7b43880 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "invalid_zone": "Ge\u00e7ersiz b\u00f6lge" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "Kay\u0131tlar" + }, + "title": "G\u00fcncellenecek Kay\u0131tlar\u0131 Se\u00e7in" + }, + "user": { + "title": "Cloudflare'ye ba\u011flan\u0131n" + }, + "zone": { + "data": { + "zone": "B\u00f6lge" + }, + "title": "G\u00fcncellenecek B\u00f6lgeyi Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/tr.json b/homeassistant/components/device_tracker/translations/tr.json index 6bb5ae1460..87042b6500 100644 --- a/homeassistant/components/device_tracker/translations/tr.json +++ b/homeassistant/components/device_tracker/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "enters": "{entity_name} bir b\u00f6lgeye girdi", + "leaves": "{entity_name} bir b\u00f6lgeden ayr\u0131l\u0131yor" + } + }, "state": { "_": { "home": "Evde", diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json new file mode 100644 index 0000000000..94c31d0e15 --- /dev/null +++ b/homeassistant/components/dsmr/translations/tr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Varl\u0131k g\u00fcncellemeleri [ler] aras\u0131ndaki minimum s\u00fcre" + }, + "title": "DSMR Se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json new file mode 100644 index 0000000000..aafc2e2b30 --- /dev/null +++ b/homeassistant/components/epson/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "name": "\u0130sim", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index 26bf8386d6..5d1e99d17f 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Raggiungi il server GIO\u015a" + "can_reach_server": "Server GIO\u015a raggiungibile" } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 937b6099bd..385a0eedff 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -5,7 +5,7 @@ "disk_total": "Disco totale", "disk_used": "Disco utilizzato", "docker_version": "Versione Docker", - "healthy": "Sano", + "healthy": "Integrit\u00e0", "host_os": "Sistema Operativo Host", "installed_addons": "Componenti aggiuntivi installati", "supervisor_api": "API Supervisore", diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index 981cb51c83..d368ac0fb3 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -1,3 +1,13 @@ { + "system_health": { + "info": { + "board": "Panel", + "disk_total": "Disk Toplam\u0131", + "disk_used": "Kullan\u0131lan Disk", + "docker_version": "Docker S\u00fcr\u00fcm\u00fc", + "healthy": "Sa\u011fl\u0131kl\u0131", + "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json new file mode 100644 index 0000000000..1ff8ea1b3a --- /dev/null +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -0,0 +1,19 @@ +{ + "system_health": { + "info": { + "arch": "CPU Mimarisi", + "dev": "Geli\u015ftirme", + "docker": "Konteyner", + "docker_version": "Konteyner", + "host_os": "Home Assistant OS", + "installation_type": "Kurulum T\u00fcr\u00fc", + "os_name": "\u0130\u015fletim Sistemi Ailesi", + "os_version": "\u0130\u015fletim Sistemi S\u00fcr\u00fcm\u00fc", + "python_version": "Python S\u00fcr\u00fcm\u00fc", + "supervisor": "S\u00fcperviz\u00f6r", + "timezone": "Saat dilimi", + "version": "S\u00fcr\u00fcm", + "virtualenv": "Sanal Ortam" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json new file mode 100644 index 0000000000..6f46000e3e --- /dev/null +++ b/homeassistant/components/hyperion/translations/tr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "auth_new_token_not_granted_error": "Hyperion UI'de yeni olu\u015fturulan belirte\u00e7 onaylanmad\u0131", + "auth_new_token_not_work_error": "Yeni olu\u015fturulan belirte\u00e7 kullan\u0131larak kimlik do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu", + "auth_required_error": "Yetkilendirmenin gerekli olup olmad\u0131\u011f\u0131 belirlenemedi", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi" + }, + "step": { + "auth": { + "data": { + "create_token": "Otomatik olarak yeni belirte\u00e7 olu\u015fturma", + "token": "Veya \u00f6nceden varolan belirte\u00e7 leri sa\u011flay\u0131n" + } + }, + "create_token": { + "title": "Otomatik olarak yeni kimlik do\u011frulama belirteci olu\u015fturun" + }, + "create_token_external": { + "title": "Hyperion kullan\u0131c\u0131 aray\u00fcz\u00fcnde yeni belirteci kabul edin" + }, + "user": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Renkler ve efektler i\u00e7in kullan\u0131lacak hyperion \u00f6nceli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/tr.json b/homeassistant/components/ipma/translations/tr.json new file mode 100644 index 0000000000..488ad37994 --- /dev/null +++ b/homeassistant/components/ipma/translations/tr.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "api_endpoint_reachable": "Ula\u015f\u0131labilir IPMA API u\u00e7 noktas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json new file mode 100644 index 0000000000..49fa9545e9 --- /dev/null +++ b/homeassistant/components/kulersky/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/de.json b/homeassistant/components/lovelace/translations/de.json new file mode 100644 index 0000000000..c8680fcb7e --- /dev/null +++ b/homeassistant/components/lovelace/translations/de.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "views": "Ansichten" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/tr.json b/homeassistant/components/lovelace/translations/tr.json new file mode 100644 index 0000000000..9f763d0d6c --- /dev/null +++ b/homeassistant/components/lovelace/translations/tr.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "dashboards": "Kontrol panelleri", + "mode": "Mod", + "resources": "Kaynaklar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json new file mode 100644 index 0000000000..545a3547ff --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "connection_error": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "IP adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 3f55b19b25..42847d89fd 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -26,5 +26,10 @@ "title": "Nest-Konto verkn\u00fcpfen" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Bewegung erkannt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json new file mode 100644 index 0000000000..484cdaff6e --- /dev/null +++ b/homeassistant/components/nest/translations/tr.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "camera_motion": "Hareket alg\u0131land\u0131", + "camera_person": "Ki\u015fi alg\u0131land\u0131", + "camera_sound": "Ses alg\u0131land\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/it.json b/homeassistant/components/nws/translations/it.json index 827d8078b5..3b651ce82b 100644 --- a/homeassistant/components/nws/translations/it.json +++ b/homeassistant/components/nws/translations/it.json @@ -15,7 +15,7 @@ "longitude": "Logitudine", "station": "Codice stazione METAR" }, - "description": "Se non viene specificato un codice di stazione METAR, la latitudine e la longitudine verranno utilizzate per trovare la stazione pi\u00f9 vicina.", + "description": "Se non \u00e8 specificato un codice stazione METAR, la latitudine e la longitudine saranno utilizzate per trovare la stazione pi\u00f9 vicina. Per ora, una chiave API pu\u00f2 essere qualsiasi cosa. Si consiglia di utilizzare un indirizzo e-mail valido.", "title": "Collegati al Servizio Meteorologico Nazionale" } } diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json new file mode 100644 index 0000000000..f3784f6de8 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "flow_title": "OVO Enerji: {username}", + "step": { + "reauth": { + "data": { + "password": "\u015eifre" + }, + "description": "OVO Energy i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.", + "title": "Yeniden kimlik do\u011frulama" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json new file mode 100644 index 0000000000..d0a70d5775 --- /dev/null +++ b/homeassistant/components/ozw/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "progress": { + "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." + }, + "step": { + "install_addon": { + "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index e2e01eb1df..2282e3584f 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -18,7 +18,8 @@ "user_gateway": { "data": { "port": "Port" - } + }, + "description": "Bitte eingeben" } } }, diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json new file mode 100644 index 0000000000..d25f1975cf --- /dev/null +++ b/homeassistant/components/plugwise/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user_gateway": { + "data": { + "username": "Smile Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/tr.json b/homeassistant/components/solaredge/translations/tr.json new file mode 100644 index 0000000000..5307276a71 --- /dev/null +++ b/homeassistant/components/solaredge/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/tr.json b/homeassistant/components/spotify/translations/tr.json index c7307d119a..c543f155e4 100644 --- a/homeassistant/components/spotify/translations/tr.json +++ b/homeassistant/components/spotify/translations/tr.json @@ -12,5 +12,10 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API u\u00e7 noktas\u0131na ula\u015f\u0131labilir" + } } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json new file mode 100644 index 0000000000..6b9d85fd9f --- /dev/null +++ b/homeassistant/components/srp_energy/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/tr.json b/homeassistant/components/srp_energy/translations/tr.json new file mode 100644 index 0000000000..1b08426f63 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r" + }, + "step": { + "user": { + "data": { + "id": "Hesap Kimli\u011fi", + "is_tou": "Kullan\u0131m Zaman\u0131 Plan\u0131 m\u0131", + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "title": "SRP Enerji" +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 0dd8a3d554..5a4de08033 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -4,8 +4,24 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { + "device": { + "data": { + "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", + "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", + "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", + "support_color": "Vurgu rengi", + "temp_divider": "S\u0131cakl\u0131k de\u011ferleri ay\u0131r\u0131c\u0131 (0 = varsay\u0131lan\u0131 kullan)", + "tuya_max_coltemp": "Cihaz taraf\u0131ndan bildirilen maksimum renk s\u0131cakl\u0131\u011f\u0131", + "unit_of_measurement": "Cihaz\u0131n kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi" + }, + "description": "{device_type} ayg\u0131t\u0131 '{device_name}' i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "Tuya Cihaz\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + }, "init": { "data": { + "discovery_interval": "Cihaz\u0131 yoklama aral\u0131\u011f\u0131 saniye cinsinden", + "list_devices": "Yap\u0131land\u0131rmay\u0131 kaydetmek i\u00e7in yap\u0131land\u0131r\u0131lacak veya bo\u015f b\u0131rak\u0131lacak cihazlar\u0131 se\u00e7in", + "query_device": "Daha h\u0131zl\u0131 durum g\u00fcncellemesi i\u00e7in sorgu y\u00f6ntemini kullanacak cihaz\u0131 se\u00e7in", "query_interval": "Ayg\u0131t yoklama aral\u0131\u011f\u0131 saniye cinsinden" }, "description": "Yoklama aral\u0131\u011f\u0131 de\u011ferlerini \u00e7ok d\u00fc\u015f\u00fck ayarlamay\u0131n, aksi takdirde \u00e7a\u011fr\u0131lar g\u00fcnl\u00fckte hata mesaj\u0131 olu\u015fturarak ba\u015far\u0131s\u0131z olur", diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json new file mode 100644 index 0000000000..14365f988b --- /dev/null +++ b/homeassistant/components/twinkly/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Twinkly cihaz\u0131n\u0131z\u0131n ana bilgisayar\u0131 (veya IP adresi)" + }, + "description": "Twinkly led dizinizi ayarlay\u0131n", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/tr.json b/homeassistant/components/water_heater/translations/tr.json new file mode 100644 index 0000000000..3010c9e622 --- /dev/null +++ b/homeassistant/components/water_heater/translations/tr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + } + } +} \ No newline at end of file From c6c3d720ca6e6562b976225183bb89c7703fa872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 15 Dec 2020 04:38:01 +0200 Subject: [PATCH 125/302] Upgrade bandit to 1.7.0 (#44184) * Upgrade bandit to 1.6.3 https://github.com/PyCQA/bandit/releases/tag/1.6.3 * Upgrade bandit to 1.7.0 https://github.com/PyCQA/bandit/releases/tag/1.7.0 --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a69f0b444..c96a990433 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - pydocstyle==5.1.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit - rev: 1.6.2 + rev: 1.7.0 hooks: - id: bandit args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a8b10eb450..e479f5e9ac 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,6 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -bandit==1.6.2 +bandit==1.7.0 black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 From 7471bf36e3f13f3da34cd7492d448ffb8c55ae37 Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Mon, 14 Dec 2020 20:10:10 -0700 Subject: [PATCH 126/302] Use new PocketCast dependency (#44007) * New PocketCast dependency * Switch to new pycketcast dependency * Update manifest.json * Alphabetized new dependency --- homeassistant/components/pocketcasts/manifest.json | 2 +- homeassistant/components/pocketcasts/sensor.py | 8 ++++---- requirements_all.txt | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index 41b46ae5cb..ad95609bd9 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -2,6 +2,6 @@ "domain": "pocketcasts", "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", - "requirements": ["pocketcasts==0.1"], + "requirements": ["pycketcasts==1.0.0"], "codeowners": [] } diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 05a8f96bda..19f7e26543 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -import pocketcasts +from pycketcasts import pocketcasts import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -29,8 +29,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) try: - api = pocketcasts.Api(username, password) - _LOGGER.debug("Found %d podcasts", len(api.my_podcasts())) + api = pocketcasts.PocketCast(email=username, password=password) + _LOGGER.debug("Found %d podcasts", len(api.subscriptions)) add_entities([PocketCastsSensor(api)], True) except OSError as err: _LOGGER.error("Connection to server failed: %s", err) @@ -63,7 +63,7 @@ class PocketCastsSensor(Entity): def update(self): """Update sensor values.""" try: - self._state = len(self._api.new_episodes_released()) + self._state = len(self._api.new_releases) _LOGGER.debug("Found %d new episodes", self._state) except OSError as err: _LOGGER.warning("Failed to contact server: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 36fab4e430..800204a2d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1149,9 +1149,6 @@ plumlightpad==0.0.11 # homeassistant.components.serial_pm pmsensor==0.4 -# homeassistant.components.pocketcasts -pocketcasts==0.1 - # homeassistant.components.poolsense poolsense==0.0.8 @@ -1309,6 +1306,9 @@ pychannels==1.0.0 # homeassistant.components.cast pychromecast==7.5.1 +# homeassistant.components.pocketcasts +pycketcasts==1.0.0 + # homeassistant.components.cmus pycmus==0.1.1 From a1e452170bc960b147afa39cf16174b31f60bc16 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Tue, 15 Dec 2020 04:45:24 +0100 Subject: [PATCH 127/302] Bump dsmr-parser to 0.25 (#44223) * Bump version in manifest * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index fdbba4212f..c3f6aa4dea 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.23"], + "requirements": ["dsmr_parser==0.25"], "codeowners": ["@Robbie1221"], "config_flow": false } diff --git a/requirements_all.txt b/requirements_all.txt index 800204a2d0..e60fcdf5dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -508,7 +508,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.23 +dsmr_parser==0.25 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8dc7f9e1fe..da4c547a4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ distro==1.5.0 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.23 +dsmr_parser==0.25 # homeassistant.components.dynalite dynalite_devices==0.1.46 From 1003464544c06405a656b40b6d27548004d903d4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 14 Dec 2020 19:52:14 -0800 Subject: [PATCH 128/302] Fix double underscore typo in fan_mode ValueError (#44182) --- homeassistant/components/nest/climate_sdm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index fbaeb502b4..368eb8b346 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -318,6 +318,6 @@ class ThermostatEntity(ClimateEntity): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode not in self.fan_modes: - raise ValueError(f"Unsupported fan__mode '{fan_mode}'") + raise ValueError(f"Unsupported fan_mode '{fan_mode}'") trait = self._device.traits[FanTrait.NAME] await trait.set_timer(FAN_INV_MODE_MAP[fan_mode]) From d4ab7880af85fa746bfd27754f3fee5c77460734 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 14 Dec 2020 20:03:02 -0800 Subject: [PATCH 129/302] Replace hard-coded domain strings with constants in the Wemo module (#44222) --- homeassistant/components/wemo/__init__.py | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index c656926ecf..d9d6a16ebf 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -7,24 +7,28 @@ import requests import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN -# Mapping from Wemo model_name to component. +# Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { - "Bridge": "light", - "CoffeeMaker": "switch", - "Dimmer": "light", - "Humidifier": "fan", - "Insight": "switch", - "LightSwitch": "switch", - "Maker": "switch", - "Motion": "binary_sensor", - "Sensor": "binary_sensor", - "Socket": "switch", + "Bridge": LIGHT_DOMAIN, + "CoffeeMaker": SWITCH_DOMAIN, + "Dimmer": LIGHT_DOMAIN, + "Humidifier": FAN_DOMAIN, + "Insight": SWITCH_DOMAIN, + "LightSwitch": SWITCH_DOMAIN, + "Maker": SWITCH_DOMAIN, + "Motion": BINARY_SENSOR_DOMAIN, + "Sensor": BINARY_SENSOR_DOMAIN, + "Socket": SWITCH_DOMAIN, } _LOGGER = logging.getLogger(__name__) @@ -135,7 +139,7 @@ async def async_setup_entry(hass, entry): device.serialnumber, ) - component = WEMO_MODEL_DISPATCH.get(device.model_name, "switch") + component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN) # Three cases: # - First time we see component, we need to load it and initialize the backlog From 69a7eff55d97dd66b78bec0fa690f71669c86c0b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 15 Dec 2020 00:59:23 -0800 Subject: [PATCH 130/302] Add tests for the Wemo __init__ module (#44196) Co-authored-by: Martin Hjelmare --- tests/components/wemo/test_init.py | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/components/wemo/test_init.py diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py new file mode 100644 index 0000000000..87a6ff3955 --- /dev/null +++ b/tests/components/wemo/test_init.py @@ -0,0 +1,89 @@ +"""Tests for the wemo component.""" +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.setup import async_setup_component + +from .conftest import MOCK_HOST, MOCK_PORT + + +async def test_config_no_config(hass): + """Component setup succeeds when there are no config entry for the domain.""" + assert await async_setup_component(hass, DOMAIN, {}) + + +async def test_config_no_static(hass): + """Component setup succeeds when there are no static config entries.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: False}}) + + +async def test_static_duplicate_static_entry(hass, pywemo_device): + """Duplicate static entries are merged into a single entity.""" + static_config_entry = f"{MOCK_HOST}:{MOCK_PORT}" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [ + static_config_entry, + static_config_entry, + ], + }, + }, + ) + await hass.async_block_till_done() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 1 + + +async def test_static_config_with_port(hass, pywemo_device): + """Static device with host and port is added and removed.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [f"{MOCK_HOST}:{MOCK_PORT}"], + }, + }, + ) + await hass.async_block_till_done() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 1 + + +async def test_static_config_without_port(hass, pywemo_device): + """Static device with host and no port is added and removed.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [MOCK_HOST], + }, + }, + ) + await hass.async_block_till_done() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 1 + + +async def test_static_config_with_invalid_host(hass): + """Component setup fails if a static host is invalid.""" + setup_success = await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [""], + }, + }, + ) + assert not setup_success From 2765c6f1e9821b2630ae27632a8d073d8225df8c Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 15 Dec 2020 05:19:57 -0800 Subject: [PATCH 131/302] Register Wemo fan services with entity service helper (#44192) * Use a singleton for the Wemo registry and fan services * Undo changes to the wemo subscription registry * Use an entity service helper to register the Wemo fan services * Fix Wemo fan test (missing ATTR_ENTITY_ID) * Use the function name directly rather than a string * Improve test coverage of the set_humidity service --- homeassistant/components/wemo/fan.py | 79 +++++++++------------------- tests/components/wemo/test_fan.py | 23 ++++++-- 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 0d5ded7b82..0dca71a0d8 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -14,8 +14,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import ATTR_ENTITY_ID -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -81,27 +80,19 @@ HASS_FAN_SPEED_TO_WEMO = { if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] } -SET_HUMIDITY_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TARGET_HUMIDITY): vol.All( - vol.Coerce(float), vol.Range(min=0, max=100) - ), - } -) - -RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) +SET_HUMIDITY_SCHEMA = { + vol.Required(ATTR_TARGET_HUMIDITY): vol.All( + vol.Coerce(float), vol.Range(min=0, max=100) + ), +} async def async_setup_entry(hass, config_entry, async_add_entities): """Set up WeMo binary sensors.""" - entities = [] async def _discovered_wemo(device): """Handle a discovered Wemo device.""" - entity = WemoHumidifier(device) - entities.append(entity) - async_add_entities([entity]) + async_add_entities([WemoHumidifier(device)]) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) @@ -112,34 +103,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] ) - def service_handle(service): - """Handle the WeMo humidifier services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) + platform = entity_platform.current_platform.get() - humidifiers = [entity for entity in entities if entity.entity_id in entity_ids] - - if service.service == SERVICE_SET_HUMIDITY: - target_humidity = service.data.get(ATTR_TARGET_HUMIDITY) - - for humidifier in humidifiers: - humidifier.set_humidity(target_humidity) - elif service.service == SERVICE_RESET_FILTER_LIFE: - for humidifier in humidifiers: - humidifier.reset_filter_life() - - # Register service(s) - hass.services.async_register( - WEMO_DOMAIN, - SERVICE_SET_HUMIDITY, - service_handle, - schema=SET_HUMIDITY_SCHEMA, + # This will call WemoHumidifier.set_humidity(target_humidity=VALUE) + platform.async_register_entity_service( + SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, WemoHumidifier.set_humidity.__name__ ) - hass.services.async_register( - WEMO_DOMAIN, - SERVICE_RESET_FILTER_LIFE, - service_handle, - schema=RESET_FILTER_LIFE_SCHEMA, + # This will call WemoHumidifier.reset_filter_life() + platform.async_register_entity_service( + SERVICE_RESET_FILTER_LIFE, {}, WemoHumidifier.reset_filter_life.__name__ ) @@ -247,21 +220,21 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): self.schedule_update_ha_state() - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, target_humidity: float) -> None: """Set the target humidity level for the Humidifier.""" - if humidity < 50: - target_humidity = WEMO_HUMIDITY_45 - elif 50 <= humidity < 55: - target_humidity = WEMO_HUMIDITY_50 - elif 55 <= humidity < 60: - target_humidity = WEMO_HUMIDITY_55 - elif 60 <= humidity < 100: - target_humidity = WEMO_HUMIDITY_60 - elif humidity >= 100: - target_humidity = WEMO_HUMIDITY_100 + if target_humidity < 50: + pywemo_humidity = WEMO_HUMIDITY_45 + elif 50 <= target_humidity < 55: + pywemo_humidity = WEMO_HUMIDITY_50 + elif 55 <= target_humidity < 60: + pywemo_humidity = WEMO_HUMIDITY_55 + elif 60 <= target_humidity < 100: + pywemo_humidity = WEMO_HUMIDITY_60 + elif target_humidity >= 100: + pywemo_humidity = WEMO_HUMIDITY_100 try: - self.wemo.set_humidity(target_humidity) + self.wemo.set_humidity(pywemo_humidity) except ActionException as err: _LOGGER.warning( "Error while setting humidity of device: %s (%s)", self.name, err diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index fe7298b40c..38055ba972 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -87,21 +87,34 @@ async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity): assert await hass.services.async_call( DOMAIN, fan.SERVICE_RESET_FILTER_LIFE, - {fan.ATTR_ENTITY_ID: wemo_entity.entity_id}, + {ATTR_ENTITY_ID: wemo_entity.entity_id}, blocking=True, ) pywemo_device.reset_filter_life.assert_called_with() -async def test_fan_set_humidity_service(hass, pywemo_device, wemo_entity): +@pytest.mark.parametrize( + "test_input,expected", + [ + (0, fan.WEMO_HUMIDITY_45), + (45, fan.WEMO_HUMIDITY_45), + (50, fan.WEMO_HUMIDITY_50), + (55, fan.WEMO_HUMIDITY_55), + (60, fan.WEMO_HUMIDITY_60), + (100, fan.WEMO_HUMIDITY_100), + ], +) +async def test_fan_set_humidity_service( + hass, pywemo_device, wemo_entity, test_input, expected +): """Verify that SERVICE_SET_HUMIDITY is registered and works.""" assert await hass.services.async_call( DOMAIN, fan.SERVICE_SET_HUMIDITY, { - fan.ATTR_ENTITY_ID: wemo_entity.entity_id, - fan.ATTR_TARGET_HUMIDITY: "50", + ATTR_ENTITY_ID: wemo_entity.entity_id, + fan.ATTR_TARGET_HUMIDITY: test_input, }, blocking=True, ) - pywemo_device.set_humidity.assert_called_with(fan.WEMO_HUMIDITY_50) + pywemo_device.set_humidity.assert_called_with(expected) From 266f82ac762ead168ba1feefd0b34ee31e7fbdf3 Mon Sep 17 00:00:00 2001 From: Tobias Perschon Date: Tue, 15 Dec 2020 15:20:08 +0100 Subject: [PATCH 132/302] Add send animation service to telegram (#41489) * added telegram sendAnimation api call Signed-off-by: Tobias Perschon * more accurate service descriptions Signed-off-by: Tobias Perschon * added 3rd parse_mode to description --- .../components/telegram_bot/__init__.py | 4 ++ .../components/telegram_bot/services.yaml | 59 +++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index fc592c9e5c..a39b6b300d 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -79,6 +79,7 @@ DOMAIN = "telegram_bot" SERVICE_SEND_MESSAGE = "send_message" SERVICE_SEND_PHOTO = "send_photo" SERVICE_SEND_STICKER = "send_sticker" +SERVICE_SEND_ANIMATION = "send_animation" SERVICE_SEND_VIDEO = "send_video" SERVICE_SEND_VOICE = "send_voice" SERVICE_SEND_DOCUMENT = "send_document" @@ -224,6 +225,7 @@ SERVICE_MAP = { SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_ANIMATION: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, @@ -367,6 +369,7 @@ async def async_setup(hass, config): elif msgtype in [ SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, + SERVICE_SEND_ANIMATION, SERVICE_SEND_VIDEO, SERVICE_SEND_VOICE, SERVICE_SEND_DOCUMENT, @@ -674,6 +677,7 @@ class TelegramNotificationService: func_send = { SERVICE_SEND_PHOTO: self.bot.sendPhoto, SERVICE_SEND_STICKER: self.bot.sendSticker, + SERVICE_SEND_ANIMATION: self.bot.sendAnimation, SERVICE_SEND_VIDEO: self.bot.sendVideo, SERVICE_SEND_VOICE: self.bot.sendVoice, SERVICE_SEND_DOCUMENT: self.bot.sendDocument, diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index a4e0adc81a..0560e6541b 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -13,7 +13,7 @@ send_message: description: An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" parse_mode: - description: "Parser for the message text: `html` or `markdown`." + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." example: "html" 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. @@ -55,6 +55,9 @@ send_photo: target: description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" 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 @@ -78,10 +81,10 @@ send_sticker: description: Send a sticker. fields: url: - description: Remote path to an webp sticker. + description: Remote path to a static .webp or animated .tgs sticker. example: "http://example.org/path/to/the/sticker.webp" file: - description: Local path to an webp sticker. + description: Local path to a static .webp or animated .tgs sticker. example: "/path/to/the/sticker.webp" username: description: Username for a URL which require HTTP basic authentication. @@ -111,6 +114,46 @@ send_sticker: description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" +send_animation: + description: Send an anmiation. + fields: + url: + description: Remote path to a GIF or H.264/MPEG-4 AVC video without sound. + example: "http://example.org/path/to/the/animation.gif" + file: + description: Local path to a GIF or H.264/MPEG-4 AVC video without sound. + example: "/path/to/the/animation.gif" + caption: + description: The title of the animation. + example: "My animation" + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + verify_ssl: + description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. + example: false + timeout: + description: Timeout for send 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"]' + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + send_video: description: Send a video. fields: @@ -118,7 +161,7 @@ send_video: description: Remote path to a video. example: "http://example.org/path/to/the/video.mp4" file: - description: Local path to an image. + description: Local path to a video. example: "/path/to/the/video.mp4" caption: description: The title of the video. @@ -132,6 +175,9 @@ send_video: target: description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" 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 @@ -212,6 +258,9 @@ send_document: target: description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" 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 @@ -275,7 +324,7 @@ edit_message: description: Optional title for your notification. Will be composed as '%title\n%message' example: "Your Garage Door Friend" parse_mode: - description: "Parser for the message text: `html` or `markdown`." + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." example: "html" disable_web_page_preview: description: Disables link previews for links in the message. From 03bfc3bcf06f101c9a91fb08871c7ebb478314b1 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 15 Dec 2020 16:04:35 +0100 Subject: [PATCH 133/302] Add Somfy climate platform (#43895) * Add climate platform * Upgrade pymfy to 0.9.3 --- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/somfy/climate.py | 214 +++++++++++++++++++ homeassistant/components/somfy/cover.py | 26 +-- homeassistant/components/somfy/manifest.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/somfy/climate.py diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 728e54b456..a831b55606 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -47,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["cover", "switch"] +SOMFY_COMPONENTS = ["cover", "switch", "climate"] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py new file mode 100644 index 0000000000..49b528645e --- /dev/null +++ b/homeassistant/components/somfy/climate.py @@ -0,0 +1,214 @@ +"""Support for Somfy Thermostat.""" + +import logging +from typing import Any, Dict, List, Optional + +from pymfy.api.devices.category import Category +from pymfy.api.devices.thermostat import ( + DurationType, + HvacState, + RegulationState, + TargetMode, + Thermostat, +) + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_HOME, + PRESET_SLEEP, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS + +from . import SomfyEntity +from .const import API, COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_CATEGORIES = {Category.HVAC.value} + +PRESET_FROST_GUARD = "Frost Guard" +PRESET_GEOFENCING = "Geofencing" +PRESET_MANUAL = "Manual" + +PRESETS_MAPPING = { + TargetMode.AT_HOME: PRESET_HOME, + TargetMode.AWAY: PRESET_AWAY, + TargetMode.SLEEP: PRESET_SLEEP, + TargetMode.MANUAL: PRESET_MANUAL, + TargetMode.GEOFENCING: PRESET_GEOFENCING, + TargetMode.FROST_PROTECTION: PRESET_FROST_GUARD, +} +REVERSE_PRESET_MAPPING = {v: k for k, v in PRESETS_MAPPING.items()} + +HVAC_MODES_MAPPING = {HvacState.COOL: HVAC_MODE_COOL, HvacState.HEAT: HVAC_MODE_HEAT} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy climate platform.""" + + def get_thermostats(): + """Retrieve thermostats.""" + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] + + return [ + SomfyClimate(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] + + async_add_entities(await hass.async_add_executor_job(get_thermostats)) + + +class SomfyClimate(SomfyEntity, ClimateEntity): + """Representation of a Somfy thermostat device.""" + + def __init__(self, coordinator, device_id, api): + """Initialize the Somfy device.""" + super().__init__(coordinator, device_id, api) + self._climate = None + self._create_device() + + def _create_device(self): + """Update the device with the latest data.""" + self._climate = Thermostat(self.device, self.api) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the state attributes of the device.""" + return {ATTR_BATTERY_LEVEL: self._climate.get_battery()} + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._climate.get_ambient_temperature() + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._climate.get_target_temperature() + + def set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + self._climate.set_target(TargetMode.MANUAL, temperature, DurationType.NEXT_MODE) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return 26.0 + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return 15.0 + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._climate.get_humidity() + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + if self._climate.get_regulation_state() == RegulationState.TIMETABLE: + return HVAC_MODE_AUTO + return HVAC_MODES_MAPPING.get(self._climate.get_hvac_state()) + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + HEAT and COOL mode are exclusive. End user has to enable a mode manually within the Somfy application. + So only one mode can be displayed. Auto mode is a scheduler. + """ + hvac_state = HVAC_MODES_MAPPING.get(self._climate.get_hvac_state()) + return [HVAC_MODE_AUTO, hvac_state] + + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode == self.hvac_mode: + return + if hvac_mode == HVAC_MODE_AUTO: + self._climate.cancel_target() + else: + self._climate.set_target( + TargetMode.MANUAL, self.target_temperature, DurationType.FURTHER_NOTICE + ) + + @property + def hvac_action(self) -> str: + """Return the current running hvac operation if supported.""" + if not self.current_temperature or not self.target_temperature: + return CURRENT_HVAC_IDLE + + if ( + self.hvac_mode == HVAC_MODE_HEAT + and self.current_temperature < self.target_temperature + ): + return CURRENT_HVAC_HEAT + + if ( + self.hvac_mode == HVAC_MODE_COOL + and self.current_temperature > self.target_temperature + ): + return CURRENT_HVAC_COOL + + return CURRENT_HVAC_IDLE + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode.""" + mode = self._climate.get_target_mode() + return PRESETS_MAPPING.get(mode) + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return list(PRESETS_MAPPING.values()) + + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if self.preset_mode == preset_mode: + return + + if preset_mode == PRESET_HOME: + temperature = self._climate.get_at_home_temperature() + elif preset_mode == PRESET_AWAY: + temperature = self._climate.get_away_temperature() + elif preset_mode == PRESET_SLEEP: + temperature = self._climate.get_night_temperature() + elif preset_mode == PRESET_FROST_GUARD: + temperature = self._climate.get_frost_protection_temperature() + elif preset_mode in [PRESET_MANUAL, PRESET_GEOFENCING]: + temperature = self.target_temperature + else: + _LOGGER.error("Preset mode not supported: %s", preset_mode) + return + + self._climate.set_target( + REVERSE_PRESET_MAPPING[preset_mode], temperature, DurationType.NEXT_MODE + ) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 696412ac3c..4542506bec 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -62,12 +62,12 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self._closed = None self._is_opening = None self._is_closing = None - self.cover = None + self._cover = None self._create_device() def _create_device(self) -> Blind: """Update the device with the latest data.""" - self.cover = Blind(self.device, self.api) + self._cover = Blind(self.device, self.api) @property def supported_features(self) -> int: @@ -97,7 +97,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self.async_write_ha_state() try: # Blocks until the close command is sent - await self.hass.async_add_executor_job(self.cover.close) + await self.hass.async_add_executor_job(self._cover.close) self._closed = True finally: self._is_closing = None @@ -109,7 +109,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self.async_write_ha_state() try: # Blocks until the open command is sent - await self.hass.async_add_executor_job(self.cover.open) + await self.hass.async_add_executor_job(self._cover.open) self._closed = False finally: self._is_opening = None @@ -117,11 +117,11 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): def stop_cover(self, **kwargs): """Stop the cover.""" - self.cover.stop() + self._cover.stop() def set_cover_position(self, **kwargs): """Move the cover shutter to a specific position.""" - self.cover.set_position(100 - kwargs[ATTR_POSITION]) + self._cover.set_position(100 - kwargs[ATTR_POSITION]) @property def device_class(self): @@ -137,7 +137,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """Return the current position of cover shutter.""" if not self.has_state("position"): return None - return 100 - self.cover.get_position() + return 100 - self._cover.get_position() @property def is_opening(self): @@ -158,7 +158,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """Return if the cover is closed.""" is_closed = None if self.has_state("position"): - is_closed = self.cover.is_closed() + is_closed = self._cover.is_closed() elif self.optimistic: is_closed = self._closed return is_closed @@ -171,23 +171,23 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """ if not self.has_state("orientation"): return None - return 100 - self.cover.orientation + return 100 - self._cover.orientation def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - self.cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] + self._cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] def open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - self.cover.orientation = 0 + self._cover.orientation = 0 def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - self.cover.orientation = 100 + self._cover.orientation = 100 def stop_cover_tilt(self, **kwargs): """Stop the cover.""" - self.cover.stop() + self._cover.stop() async def async_added_to_hass(self): """Complete the initialization.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 69450c4c4d..ea84bf3458 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.9.1"] -} + "requirements": ["pymfy==0.9.3"] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index e60fcdf5dd..728dc8ca34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1521,7 +1521,7 @@ pymediaroom==0.6.4.1 pymelcloud==2.5.2 # homeassistant.components.somfy -pymfy==0.9.1 +pymfy==0.9.3 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da4c547a4f..1d0a8c6fa5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -770,7 +770,7 @@ pymata-express==1.19 pymelcloud==2.5.2 # homeassistant.components.somfy -pymfy==0.9.1 +pymfy==0.9.3 # homeassistant.components.mochad pymochad==0.2.0 From a0ce541c0dce57614093653def0c5052aab7d3d4 Mon Sep 17 00:00:00 2001 From: Tim Messerschmidt Date: Tue, 15 Dec 2020 16:38:32 +0100 Subject: [PATCH 134/302] Bump google-nest-sdm to 0.2.1 to support more SDM Pub/Sub realms (#44163) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 60293612cd..028f56587a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0", - "google-nest-sdm==0.2.0" + "google-nest-sdm==0.2.1" ], "codeowners": [ "@awarecan", diff --git a/requirements_all.txt b/requirements_all.txt index 728dc8ca34..3ae5d51161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -684,7 +684,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.0 +google-nest-sdm==0.2.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d0a8c6fa5..7f9a9ad69b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.0 +google-nest-sdm==0.2.1 # homeassistant.components.gree greeclimate==0.10.3 From e601f6241625a7051abad456fa978efb14e8c986 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 15 Dec 2020 10:44:28 -0500 Subject: [PATCH 135/302] Default smartenergy multiplier and divisor (#44257) --- homeassistant/components/zha/core/channels/smartenergy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 0bd7159cf9..32e4902799 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -89,12 +89,12 @@ class Metering(ZigbeeChannel): @property def divisor(self) -> int: """Return divisor for the value.""" - return self.cluster.get("divisor") + return self.cluster.get("divisor") or 1 @property def multiplier(self) -> int: """Return multiplier for the value.""" - return self.cluster.get("multiplier") + return self.cluster.get("multiplier") or 1 def async_configure_channel_specific(self) -> Coroutine: """Configure channel.""" From 0aa85b2321243e22fbf94e2f42fc9fa340d31475 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Dec 2020 17:23:00 +0100 Subject: [PATCH 136/302] Bump hatasmota to 0.1.6 (#44226) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a4c6f77fc1..5b298d44ce 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.4"], + "requirements": ["hatasmota==0.1.6"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 3ae5d51161..50ee26f7fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.39.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.4 +hatasmota==0.1.6 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f9a9ad69b..5389f0993d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.39.0 # homeassistant.components.tasmota -hatasmota==0.1.4 +hatasmota==0.1.6 # homeassistant.components.jewish_calendar hdate==0.9.12 From 4880a1d55a12c5ae0f8456a828aff69819f791d5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 15 Dec 2020 20:25:14 +0200 Subject: [PATCH 137/302] Change shelly CONNECTION_CLASS to CONN_CLASS_LOCAL_PUSH (#44260) Shelly integration is using local push since HA 0.118 --- homeassistant/components/shelly/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 88e01f04bc..40ac452a9d 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -65,7 +65,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH host = None info = None From 3bb996c5b4d7b6f16afa2777dee82fd26b8a84be Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 16 Dec 2020 00:03:31 +0000 Subject: [PATCH 138/302] [ci skip] Translation update --- .../components/abode/translations/de.json | 5 +++++ .../components/apple_tv/translations/de.json | 13 +++++++++++++ .../components/bsblan/translations/de.json | 4 +++- .../components/epson/translations/de.json | 11 +++++++++++ .../fireservicerota/translations/de.json | 16 ++++++++++++++++ .../homeassistant/translations/de.json | 10 ++++++++++ .../components/ovo_energy/translations/de.json | 5 +++++ .../components/srp_energy/translations/de.json | 7 +++++++ .../components/tuya/translations/de.json | 3 +++ .../components/twinkly/translations/de.json | 9 +++++++++ 10 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/apple_tv/translations/de.json create mode 100644 homeassistant/components/epson/translations/de.json create mode 100644 homeassistant/components/fireservicerota/translations/de.json create mode 100644 homeassistant/components/homeassistant/translations/de.json create mode 100644 homeassistant/components/twinkly/translations/de.json diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json index b0f17918cd..a5d3686886 100644 --- a/homeassistant/components/abode/translations/de.json +++ b/homeassistant/components/abode/translations/de.json @@ -4,6 +4,11 @@ "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json new file mode 100644 index 0000000000..b1ed434e21 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "flow_title": "Apple TV: {name}", + "step": { + "pair_with_pin": { + "data": { + "pin": "PIN-Code" + } + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 39be96b84d..4b4aa37640 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -7,7 +7,9 @@ "user": { "data": { "host": "Host", - "port": "Port Nummer" + "password": "Passwort", + "port": "Port Nummer", + "username": "Benutzername" } } } diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json new file mode 100644 index 0000000000..82687d50bf --- /dev/null +++ b/homeassistant/components/epson/translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/de.json b/homeassistant/components/fireservicerota/translations/de.json new file mode 100644 index 0000000000..35636c0fd9 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "Passwort" + } + }, + "user": { + "data": { + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json new file mode 100644 index 0000000000..45768a9f12 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/de.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 6f39806287..7ba59cf064 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -1,6 +1,11 @@ { "config": { "step": { + "reauth": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json index 6b9d85fd9f..23fe89c73b 100644 --- a/homeassistant/components/srp_energy/translations/de.json +++ b/homeassistant/components/srp_energy/translations/de.json @@ -2,6 +2,13 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "password": "Passwort" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 07e72a2960..496c279ba2 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -13,6 +13,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Fehler beim Verbinden" + }, "error": { "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", "dev_not_found": "Ger\u00e4t nicht gefunden" diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json new file mode 100644 index 0000000000..e702c18e89 --- /dev/null +++ b/homeassistant/components/twinkly/translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Twinkly" + } + } + } +} \ No newline at end of file From dfde4039376274230aad5170b5c6959b94463404 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Dec 2020 11:00:22 +0100 Subject: [PATCH 139/302] Remove Home Assistant Cast user when removing entry (#44228) * Remove Home Assistant Cast user when removing entry * Fix test * Fix test --- homeassistant/components/cast/__init__.py | 5 ++++ .../components/cast/home_assistant_cast.py | 11 +++++++ .../cast/test_home_assistant_cast.py | 30 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 4dfb58ef3b..49cec20776 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -29,3 +29,8 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, "media_player") ) return True + + +async def async_remove_entry(hass, entry): + """Remove Home Assistant Cast user.""" + await home_assistant_cast.async_remove_user(hass, entry) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 28672ef409..3edc1ce2cd 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -72,3 +72,14 @@ async def async_setup_ha_cast( } ), ) + + +async def async_remove_user( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Remove Home Assistant Cast user.""" + user_id: Optional[str] = entry.data.get("user_id") + + if user_id is not None: + user = await hass.auth.async_get_user(user_id) + await hass.auth.async_remove_user(user) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 2fff760bb7..8ddb6e82ed 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,6 @@ """Test Home Assistant Cast.""" +from homeassistant import config_entries from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config @@ -86,3 +87,32 @@ async def test_use_cloud_url(hass, mock_zeroconf): assert len(calls) == 1 controller = calls[0][0] assert controller.hass_url == "https://something.nabu.casa" + + +async def test_remove_entry(hass, mock_zeroconf): + """Test removing config entry removes user.""" + entry = MockConfigEntry( + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={}, + domain="cast", + title="Google Cast", + ) + + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.cast.media_player._async_setup_platform" + ), patch( + "pychromecast.discovery.discover_chromecasts", return_value=(True, None) + ), patch( + "pychromecast.discovery.stop_discovery" + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "cast" in hass.config.components + + user_id = entry.data.get("user_id") + assert await hass.auth.async_get_user(user_id) + + assert await hass.config_entries.async_remove(entry.entry_id) + assert not await hass.auth.async_get_user(user_id) From 4f9d5792ed327482fec364f92064331eaddbc663 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Dec 2020 11:18:50 +0100 Subject: [PATCH 140/302] Fix setting timestamp on input_datetime (#44274) --- .../components/input_datetime/__init__.py | 8 ++---- tests/components/input_datetime/test_init.py | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index aa1f0b8814..195e4c2242 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -365,9 +365,7 @@ class InputDatetime(RestoreEntity): def async_set_datetime(self, date=None, time=None, datetime=None, timestamp=None): """Set a new date / time.""" if timestamp: - datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp)).replace( - tzinfo=None - ) + datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp)) if datetime: date = datetime.date() @@ -388,8 +386,8 @@ class InputDatetime(RestoreEntity): if not time: time = self._current_datetime.time() - self._current_datetime = py_datetime.datetime.combine(date, time).replace( - tzinfo=dt_util.DEFAULT_TIME_ZONE + self._current_datetime = dt_util.DEFAULT_TIME_ZONE.localize( + py_datetime.datetime.combine(date, time) ) self.async_write_ha_state() diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index d40a88e3f4..a336ef8236 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -697,6 +697,15 @@ async def test_timestamp(hass): ).strftime(FMT_DATETIME) == "2020-12-13 10:00:00" ) + # Use datetime.datetime.fromtimestamp + assert ( + dt_util.as_local( + datetime.datetime.fromtimestamp( + state_without_tz.attributes[ATTR_TIMESTAMP] + ) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) # Test initial time sets timestamp correctly. state_time = hass.states.get("input_datetime.test_time_initial") @@ -704,5 +713,24 @@ async def test_timestamp(hass): assert state_time.state == "10:00:00" assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 + # Test that setting the timestamp of an entity works. + await hass.services.async_call( + DOMAIN, + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.test_datetime_initial_with_tz", + ATTR_TIMESTAMP: state_without_tz.attributes[ATTR_TIMESTAMP], + }, + blocking=True, + ) + state_with_tz_updated = hass.states.get( + "input_datetime.test_datetime_initial_with_tz" + ) + assert state_with_tz_updated.state == "2020-12-13 10:00:00" + assert ( + state_with_tz_updated.attributes[ATTR_TIMESTAMP] + == state_without_tz.attributes[ATTR_TIMESTAMP] + ) + finally: dt_util.set_default_time_zone(ORIG_TIMEZONE) From 295b1a91c3017258b2743f951e2d2bdb2a5690da Mon Sep 17 00:00:00 2001 From: Geoffrey Lagaisse <51802836+geoffreylagaisse@users.noreply.github.com> Date: Wed, 16 Dec 2020 15:53:01 +0100 Subject: [PATCH 141/302] Bump python-qbittorrent to 0.4.2 (#44268) --- CODEOWNERS | 1 + homeassistant/components/qbittorrent/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8e2dd1fa56..e5d67234f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -356,6 +356,7 @@ homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/pvpc_hourly_pricing/* @azogue +homeassistant/components/qbittorrent/* @geoffreylagaisse homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index c839cb7593..2f3e8cf4f1 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -2,6 +2,6 @@ "domain": "qbittorrent", "name": "qBittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", - "requirements": ["python-qbittorrent==0.4.1"], - "codeowners": [] + "requirements": ["python-qbittorrent==0.4.2"], + "codeowners": ["@geoffreylagaisse"] } diff --git a/requirements_all.txt b/requirements_all.txt index 50ee26f7fd..eaf21edabf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1801,7 +1801,7 @@ python-nmap==0.6.1 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.qbittorrent -python-qbittorrent==0.4.1 +python-qbittorrent==0.4.2 # homeassistant.components.ripple python-ripple-api==0.0.3 From 36eebd92ddca09272c3b9f6bdc3519835437b401 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Dec 2020 18:29:57 +0100 Subject: [PATCH 142/302] Bump pychromecast to 7.6.0 (#44289) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 3a795b6042..8072e06c2e 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.5.1"], + "requirements": ["pychromecast==7.6.0"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index eaf21edabf..80f685845a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.5.1 +pychromecast==7.6.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5389f0993d..fef2cba80d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -661,7 +661,7 @@ pybotvac==0.0.17 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==7.5.1 +pychromecast==7.6.0 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 From fd24baa1f6bfaab7cc7f14a5faa1ce71a6c3bff4 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 16 Dec 2020 22:28:59 +0200 Subject: [PATCH 143/302] Fix Shelly devices missing properties field (#44279) --- .../components/shelly/config_flow.py | 8 +---- tests/components/shelly/test_config_flow.py | 30 +------------------ 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 40ac452a9d..261c1898ca 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -27,12 +27,6 @@ HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) HTTP_CONNECT_ERRORS = (asyncio.TimeoutError, aiohttp.ClientError) -def _remove_prefix(shelly_str): - if shelly_str.startswith("shellyswitch"): - return shelly_str[6:] - return shelly_str - - async def validate_input(hass: core.HomeAssistant, host, data): """Validate the user input allows us to connect. @@ -159,7 +153,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = zeroconf_info["host"] # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - "name": _remove_prefix(zeroconf_info["properties"]["id"]) + "name": zeroconf_info.get("name", "").split(".")[0] } return await self.async_step_confirm_discovery() diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 75ac015b83..71c971757c 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -20,11 +20,6 @@ DISCOVERY_INFO = { "name": "shelly1pm-12345", "properties": {"id": "shelly1pm-12345"}, } -SWITCH25_DISCOVERY_INFO = { - "host": "1.1.1.1", - "name": "shellyswitch25-12345", - "properties": {"id": "shellyswitch25-12345"}, -} async def test_form(hass): @@ -67,7 +62,7 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_title_without_name_and_prefix(hass): +async def test_title_without_name(hass): """Test we set the title to the hostname when the device doesn't have a name.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -360,29 +355,6 @@ async def test_zeroconf(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_with_switch_prefix(hass): - """Test we get remove shelly from the prefix.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "aioshelly.get_info", - return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=SWITCH25_DISCOVERY_INFO, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - assert result["type"] == "form" - assert result["errors"] == {} - context = next( - flow["context"] - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == result["flow_id"] - ) - assert context["title_placeholders"]["name"] == "switch25-12345" - - @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From d0ebc006843b9283c593682db0cdaf2af8c4e354 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 16 Dec 2020 23:39:41 +0100 Subject: [PATCH 144/302] Add OAuth to Neato (#44031) Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + homeassistant/components/neato/__init__.py | 162 +++++----- homeassistant/components/neato/api.py | 55 ++++ homeassistant/components/neato/camera.py | 2 +- homeassistant/components/neato/config_flow.py | 137 +++------ homeassistant/components/neato/const.py | 2 - homeassistant/components/neato/manifest.json | 14 +- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/neato/strings.json | 31 +- homeassistant/components/neato/switch.py | 2 +- .../components/neato/translations/de.json | 24 +- .../components/neato/translations/en.json | 29 +- homeassistant/components/neato/vacuum.py | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/neato/test_config_flow.py | 278 +++++++++--------- tests/components/neato/test_init.py | 118 -------- 17 files changed, 363 insertions(+), 504 deletions(-) create mode 100644 homeassistant/components/neato/api.py delete mode 100644 tests/components/neato/test_init.py diff --git a/.coveragerc b/.coveragerc index 01c0a657f3..a8459a2cd7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -567,6 +567,8 @@ omit = homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py + homeassistant/components/neato/__init__.py + homeassistant/components/neato/api.py homeassistant/components/neato/camera.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 9775dc592f..1d9d3de4f8 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -3,26 +3,30 @@ import asyncio from datetime import timedelta import logging -from pybotvac import Account, Neato, Vorwerk -from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException +from pybotvac import Account, Neato +from pybotvac.exceptions import NeatoException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_SOURCE, + CONF_TOKEN, +) from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle -from .config_flow import NeatoConfigFlow +from . import api, config_flow from .const import ( - CONF_VENDOR, NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, - VALID_VENDORS, ) _LOGGER = logging.getLogger(__name__) @@ -32,82 +36,74 @@ CONFIG_SCHEMA = vol.Schema( { NEATO_DOMAIN: vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, } ) }, extra=vol.ALLOW_EXTRA, ) +PLATFORMS = ["camera", "vacuum", "switch", "sensor"] -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the Neato component.""" + hass.data[NEATO_DOMAIN] = {} if NEATO_DOMAIN not in config: - # There is an entry and nothing in configuration.yaml return True - entries = hass.config_entries.async_entries(NEATO_DOMAIN) hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN] - - if entries: - # There is an entry and something in the configuration.yaml - entry = entries[0] - conf = config[NEATO_DOMAIN] - if ( - entry.data[CONF_USERNAME] == conf[CONF_USERNAME] - and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD] - and entry.data[CONF_VENDOR] == conf[CONF_VENDOR] - ): - # The entry is not outdated - return True - - # The entry is outdated - error = await hass.async_add_executor_job( - NeatoConfigFlow.try_login, - conf[CONF_USERNAME], - conf[CONF_PASSWORD], - conf[CONF_VENDOR], - ) - if error is not None: - _LOGGER.error(error) - return False - - # Update the entry - hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN]) - else: - # Create the new entry - hass.async_create_task( - hass.config_entries.flow.async_init( - NEATO_DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[NEATO_DOMAIN], - ) - ) + vendor = Neato() + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + api.NeatoImplementation( + hass, + NEATO_DOMAIN, + config[NEATO_DOMAIN][CONF_CLIENT_ID], + config[NEATO_DOMAIN][CONF_CLIENT_SECRET], + vendor.auth_endpoint, + vendor.token_endpoint, + ), + ) return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up config entry.""" - hub = NeatoHub(hass, entry.data, Account) - - await hass.async_add_executor_job(hub.login) - if not hub.logged_in: - _LOGGER.debug("Failed to login to Neato API") + if CONF_TOKEN not in entry.data: + # Init reauth flow + hass.async_create_task( + hass.config_entries.flow.async_init( + NEATO_DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + ) + ) return False + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + neato_session = api.ConfigEntryAuth(hass, entry, session) + hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session + hub = NeatoHub(hass, Account(neato_session)) + try: await hass.async_add_executor_job(hub.update_robots) - except NeatoRobotException as ex: + except NeatoException as ex: _LOGGER.debug("Failed to connect to Neato API") raise ConfigEntryNotReady from ex hass.data[NEATO_LOGIN] = hub - for component in ("camera", "vacuum", "switch", "sensor"): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -115,53 +111,27 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: """Unload config entry.""" - hass.data.pop(NEATO_LOGIN) - await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, "camera"), - hass.config_entries.async_forward_entry_unload(entry, "vacuum"), - hass.config_entries.async_forward_entry_unload(entry, "switch"), - hass.config_entries.async_forward_entry_unload(entry, "sensor"), + unload_functions = ( + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ) - return True + + unload_ok = all(await asyncio.gather(*unload_functions)) + if unload_ok: + hass.data[NEATO_DOMAIN].pop(entry.entry_id) + + return unload_ok class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass, domain_config, neato): + def __init__(self, hass: HomeAssistantType, neato: Account): """Initialize the Neato hub.""" - self.config = domain_config - self._neato = neato - self._hass = hass - - if self.config[CONF_VENDOR] == "vorwerk": - self._vendor = Vorwerk() - else: # Neato - self._vendor = Neato() - - self.my_neato = None - self.logged_in = False - - def login(self): - """Login to My Neato.""" - _LOGGER.debug("Trying to connect to Neato API") - try: - self.my_neato = self._neato( - self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor - ) - except NeatoException as ex: - if isinstance(ex, NeatoLoginException): - _LOGGER.error("Invalid credentials") - else: - _LOGGER.error("Unable to connect to Neato API") - raise ConfigEntryNotReady from ex - self.logged_in = False - return - - self.logged_in = True - _LOGGER.debug("Successfully connected to Neato API") + self._hass: HomeAssistantType = hass + self.my_neato: Account = neato @Throttle(timedelta(minutes=1)) def update_robots(self): diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py new file mode 100644 index 0000000000..931d7cdb71 --- /dev/null +++ b/homeassistant/components/neato/api.py @@ -0,0 +1,55 @@ +"""API for Neato Botvac bound to Home Assistant OAuth.""" +from asyncio import run_coroutine_threadsafe +import logging + +import pybotvac + +from homeassistant import config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryAuth(pybotvac.OAuthSession): + """Provide Neato Botvac authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Neato Botvac Auth.""" + self.hass = hass + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token, vendor=pybotvac.Neato()) + + def refresh_tokens(self) -> str: + """Refresh and return new Neato Botvac tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token["access_token"] + + +class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): + """Neato implementation of LocalOAuth2Implementation. + + We need this class because we have to add client_secret and scope to the authorization request. + """ + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"client_secret": self.client_secret} + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize. + + We must make sure that the plus signs are not encoded. + """ + url = await super().async_generate_authorize_url(flow_id) + return f"{url}&scope=public_profile+control_robots+maps" diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 4d7c4129d8..1698a1d944 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -45,7 +45,7 @@ class NeatoCleaningMap(Camera): self.robot = robot self.neato = neato self._mapdata = mapdata - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato is not None self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial self._generated_at = None diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index f74364bc8b..449de72b15 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -1,112 +1,65 @@ -"""Config flow to configure Neato integration.""" - +"""Config flow for Neato Botvac.""" import logging +from typing import Optional -from pybotvac import Account, Neato, Vorwerk -from pybotvac.exceptions import NeatoLoginException, NeatoRobotException import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_TOKEN +from homeassistant.helpers import config_entry_oauth2_flow # pylint: disable=unused-import -from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS - -DOCS_URL = "https://www.home-assistant.io/integrations/neato" -DEFAULT_VENDOR = "neato" +from .const import NEATO_DOMAIN _LOGGER = logging.getLogger(__name__) -class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): - """Neato integration config flow.""" +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=NEATO_DOMAIN +): + """Config flow to handle Neato Botvac OAuth2 authentication.""" - VERSION = 1 + DOMAIN = NEATO_DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize flow.""" - self._username = vol.UNDEFINED - self._password = vol.UNDEFINED - self._vendor = vol.UNDEFINED + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - errors = {} - - if self._async_current_entries(): + async def async_step_user(self, user_input: Optional[dict] = None) -> dict: + """Create an entry for the flow.""" + current_entries = self._async_current_entries() + if current_entries and CONF_TOKEN in current_entries[0].data: + # Already configured return self.async_abort(reason="already_configured") - if user_input is not None: - self._username = user_input["username"] - self._password = user_input["password"] - self._vendor = user_input["vendor"] + return await super().async_step_user(user_input=user_input) - error = await self.hass.async_add_executor_job( - self.try_login, self._username, self._password, self._vendor + async def async_step_reauth(self, data) -> dict: + """Perform reauth upon migration of old entries.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: Optional[dict] = None + ) -> dict: + """Confirm reauth upon migration of old entries.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=vol.Schema({}) ) - if error: - errors["base"] = error - else: - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data=user_input, - description_placeholders={"docs_url": DOCS_URL}, - ) + return await self.async_step_user() - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), - } - ), - description_placeholders={"docs_url": DOCS_URL}, - errors=errors, - ) - - async def async_step_import(self, user_input): - """Import a config flow from configuration.""" - - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - vendor = user_input[CONF_VENDOR] - - error = await self.hass.async_add_executor_job( - self.try_login, username, password, vendor - ) - if error is not None: - _LOGGER.error(error) - return self.async_abort(reason=error) - - return self.async_create_entry( - title=f"{username} (from configuration)", - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_VENDOR: vendor, - }, - ) - - @staticmethod - def try_login(username, password, vendor): - """Try logging in to device and return any errors.""" - this_vendor = None - if vendor == "vorwerk": - this_vendor = Vorwerk() - else: # Neato - this_vendor = Neato() - - try: - Account(username, password, this_vendor) - except NeatoLoginException: - return "invalid_auth" - except NeatoRobotException: - return "unknown" - - return None + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. Update an entry if one already exist.""" + current_entries = self._async_current_entries() + if current_entries and CONF_TOKEN not in current_entries[0].data: + # Update entry + self.hass.config_entries.async_update_entry( + current_entries[0], title=self.flow_impl.name, data=data + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(current_entries[0].entry_id) + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self.flow_impl.name, data=data) diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 53948e2b19..248e455b6d 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -11,8 +11,6 @@ NEATO_ROBOTS = "neato_robots" SCAN_INTERVAL_MINUTES = 1 -VALID_VENDORS = ["neato", "vorwerk"] - MODE = {1: "Eco", 2: "Turbo"} ACTION = { diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index d36e3fa503..d3ea8a8525 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,6 +3,14 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": ["pybotvac==0.0.17"], - "codeowners": ["@dshokouhi", "@Santobert"] -} + "requirements": [ + "pybotvac==0.0.19" + ], + "codeowners": [ + "@dshokouhi", + "@Santobert" + ], + "dependencies": [ + "http" + ] +} \ No newline at end of file diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index efcbfb8d54..b083ec1d7d 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -37,7 +37,7 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self._available = neato.logged_in if neato is not None else False + self._available = neato is not None self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index 5d71d4889a..21af0f91d1 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -1,26 +1,23 @@ { "config": { "step": { - "user": { - "title": "Neato Account Info", - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "vendor": "Vendor" - }, - "description": "See [Neato documentation]({docs_url})." + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::description::confirm_setup%]" } }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "abort": { + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { - "default": "See [Neato documentation]({docs_url})." - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "default": "[%key:common::config_flow::create_entry::authenticated%]" } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index a6aa19abe2..204adb108a 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -40,7 +40,7 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self._available = neato.logged_in if neato is not None else False + self._available = neato is not None self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index c41d4e6d93..c8dcc93500 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -1,21 +1,23 @@ { "config": { "abort": { - "already_configured": "Bereits konfiguriert" + "already_configured": "Konto ist bereits konfiguriert.", + "authorize_url_timeout": "Timeout beim Erzeugen der Autorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachten Sie die Dokumentation.", + "no_url_available": "Keine URL verfügbar. Informationen zu diesem Fehler finden Sie [im Hilfebereich]({docs_url})", + "reauth_successful": "Re-Authentifizierung war erfolgreich" }, "create_entry": { - "default": "Siehe [Neato-Dokumentation]({docs_url})." + "default": "Erfolgreich authentifiziert" }, "step": { - "user": { - "data": { - "password": "Passwort", - "username": "Benutzername", - "vendor": "Hersteller" - }, - "description": "Siehe [Neato-Dokumentation]({docs_url}).", - "title": "Neato-Kontoinformationen" + "pick_implementation": { + "title": "Authentifizierungsmethode auswählen" + }, + "reauth_confirm": { + "title": "Einrichtung bestätigen?" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json index 61b6ad44df..333c8a980f 100644 --- a/homeassistant/components/neato/translations/en.json +++ b/homeassistant/components/neato/translations/en.json @@ -1,26 +1,23 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "invalid_auth": "Invalid authentication" + "already_configured": "Account is already configured.", + "authorize_url_timeout": "Timeout generating authorize URL.", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "reauth_successful": "Re-authentication was successful" }, "create_entry": { - "default": "See [Neato documentation]({docs_url})." - }, - "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "default": "Successfully authenticated" }, "step": { - "user": { - "data": { - "password": "Password", - "username": "Username", - "vendor": "Vendor" - }, - "description": "See [Neato documentation]({docs_url}).", - "title": "Neato Account Info" + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "title": "Confirm setup?" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 677bed1565..ce4156244b 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -24,7 +24,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE +from homeassistant.const import ATTR_MODE from homeassistant.helpers import config_validation as cv, entity_platform from .const import ( @@ -93,7 +93,6 @@ async def async_setup_entry(hass, entry, async_add_entities): platform.async_register_entity_service( "custom_cleaning", { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_MODE, default=2): cv.positive_int, vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, @@ -109,7 +108,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self._available = neato.logged_in if neato is not None else False + self._available = neato is not None self._mapdata = mapdata self._name = f"{self.robot.name}" self._robot_has_map = self.robot.has_persistent_maps diff --git a/requirements_all.txt b/requirements_all.txt index 80f685845a..20e3c4c115 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1292,7 +1292,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.17 +pybotvac==0.0.19 # homeassistant.components.nissan_leaf pycarwings2==2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fef2cba80d..27fe91d0eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ pyatv==0.7.5 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.17 +pybotvac==0.0.19 # homeassistant.components.cloudflare pycfdns==1.2.1 diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 31c2cddd09..6954eb1b7a 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,160 +1,156 @@ -"""Tests for the Neato config flow.""" -from pybotvac.exceptions import NeatoLoginException, NeatoRobotException -import pytest +"""Test the Neato Botvac config flow.""" +from pybotvac.neato import Neato -from homeassistant import data_entry_flow -from homeassistant.components.neato import config_flow -from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.neato.const import NEATO_DOMAIN +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.typing import HomeAssistantType from tests.async_mock import patch from tests.common import MockConfigEntry -USERNAME = "myUsername" -PASSWORD = "myPassword" -VENDOR_NEATO = "neato" -VENDOR_VORWERK = "vorwerk" -VENDOR_INVALID = "invalid" +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + +VENDOR = Neato() +OAUTH2_AUTHORIZE = VENDOR.auth_endpoint +OAUTH2_TOKEN = VENDOR.token_endpoint -@pytest.fixture(name="account") -def mock_controller_login(): - """Mock a successful login.""" - with patch("homeassistant.components.neato.config_flow.Account", return_value=True): - yield - - -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.NeatoConfigFlow() - flow.hass = hass - return flow - - -async def test_user(hass, account): - """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_VENDOR] == VENDOR_NEATO - - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_VENDOR] == VENDOR_VORWERK - - -async def test_import(hass, account): - """Test import step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == f"{USERNAME} (from configuration)" - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_VENDOR] == VENDOR_NEATO - - -async def test_abort_if_already_setup(hass, account): - """Test we abort if Neato is already setup.""" - flow = init_config_flow(hass) - MockConfigEntry( - domain=NEATO_DOMAIN, - data={ - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "neato", + { + "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, }, + ) + + result = await hass.config_entries.flow.async_init( + "neato", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + f"&client_secret={CLIENT_SECRET}" + "&scope=public_profile+control_robots+maps" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.neato.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + +async def test_abort_if_already_setup(hass: HomeAssistantType): + """Test we abort if Neato is already setup.""" + entry = MockConfigEntry( + domain=NEATO_DOMAIN, + data={"auth_implementation": "neato", "token": {"some": "data"}}, + ) + entry.add_to_hass(hass) + + # Should fail + result = await hass.config_entries.flow.async_init( + "neato", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth( + hass: HomeAssistantType, aiohttp_client, aioclient_mock, current_request_with_host +): + """Test initialization of the reauth flow.""" + assert await setup.async_setup_component( + hass, + "neato", + { + "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + MockConfigEntry( + entry_id="my_entry", + domain=NEATO_DOMAIN, + data={"username": "abcdef", "password": "123456", "vendor": "neato"}, ).add_to_hass(hass) - # Should fail, same USERNAME (import) - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + # Should show form + result = await hass.config_entries.flow.async_init( + "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" - # Should fail, same USERNAME (flow) - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + # Confirm reauth flow + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 -async def test_abort_on_invalid_credentials(hass): - """Test when we have invalid credentials.""" - flow = init_config_flow(hass) + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + # Update entry with patch( - "homeassistant.components.neato.config_flow.Account", - side_effect=NeatoLoginException(), - ): - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + "homeassistant.components.neato.async_setup_entry", return_value=True + ) as mock_setup: + result3 = await hass.config_entries.flow.async_configure(result2["flow_id"]) + await hass.async_block_till_done() - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" + new_entry = hass.config_entries.async_get_entry("my_entry") - -async def test_abort_on_unexpected_error(hass): - """Test when we have an unexpected error.""" - flow = init_config_flow(hass) - - with patch( - "homeassistant.components.neato.config_flow.Account", - side_effect=NeatoRobotException(), - ): - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "unknown" + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful" + assert new_entry.state == "loaded" + assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py deleted file mode 100644 index 182ef98e52..0000000000 --- a/tests/components/neato/test_init.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Tests for the Neato init file.""" -from pybotvac.exceptions import NeatoLoginException -import pytest - -from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component - -from tests.async_mock import patch -from tests.common import MockConfigEntry - -USERNAME = "myUsername" -PASSWORD = "myPassword" -VENDOR_NEATO = "neato" -VENDOR_VORWERK = "vorwerk" -VENDOR_INVALID = "invalid" - -VALID_CONFIG = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, -} - -DIFFERENT_CONFIG = { - CONF_USERNAME: "anotherUsername", - CONF_PASSWORD: "anotherPassword", - CONF_VENDOR: VENDOR_VORWERK, -} - -INVALID_CONFIG = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_INVALID, -} - - -@pytest.fixture(name="config_flow") -def mock_config_flow_login(): - """Mock a successful login.""" - with patch("homeassistant.components.neato.config_flow.Account", return_value=True): - yield - - -@pytest.fixture(name="hub") -def mock_controller_login(): - """Mock a successful login.""" - with patch("homeassistant.components.neato.Account", return_value=True): - yield - - -async def test_no_config_entry(hass): - """There is nothing in configuration.yaml.""" - res = await async_setup_component(hass, NEATO_DOMAIN, {}) - assert res is True - - -async def test_create_valid_config_entry(hass, config_flow, hub): - """There is something in configuration.yaml.""" - assert hass.config_entries.async_entries(NEATO_DOMAIN) == [] - assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO - - -async def test_config_entries_in_sync(hass, hub): - """The config entry and configuration.yaml are in sync.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) - - assert hass.config_entries.async_entries(NEATO_DOMAIN) - assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO - - -async def test_config_entries_not_in_sync(hass, config_flow, hub): - """The config entry and configuration.yaml are not in sync.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass) - - assert hass.config_entries.async_entries(NEATO_DOMAIN) - assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO - - -async def test_config_entries_not_in_sync_error(hass): - """The config entry and configuration.yaml are not in sync, the new configuration is wrong.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) - - assert hass.config_entries.async_entries(NEATO_DOMAIN) - with patch( - "homeassistant.components.neato.config_flow.Account", - side_effect=NeatoLoginException(), - ): - assert not await async_setup_component( - hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG} - ) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO From aaae452d58be3df7db1a89a16f48639170debdbe Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 16 Dec 2020 14:55:31 -0800 Subject: [PATCH 145/302] Add reauth step to Hyperion config flow (#43797) Co-authored-by: Martin Hjelmare --- homeassistant/components/hyperion/__init__.py | 50 ++++++++-- .../components/hyperion/config_flow.py | 50 ++++++++-- homeassistant/components/hyperion/const.py | 2 - homeassistant/components/hyperion/light.py | 3 +- .../components/hyperion/strings.json | 3 +- tests/components/hyperion/__init__.py | 45 +++++++-- tests/components/hyperion/test_config_flow.py | 77 +++++++++++++--- tests/components/hyperion/test_light.py | 91 ++++++++++++++++++- 8 files changed, 275 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 13dd977b7d..05494d1486 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -8,8 +8,8 @@ from hyperion import client, const as hyperion_const from pkg_resources import parse_version from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -85,6 +85,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +async def _create_reauth_flow( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, data=config_entry.data + ) + ) + + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hyperion from a config entry.""" host = config_entry.data[CONF_HOST] @@ -92,8 +103,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b token = config_entry.data.get(CONF_TOKEN) hyperion_client = await async_create_connect_hyperion_client( - host, port, token=token + host, port, token=token, raw_connection=True ) + + # Client won't connect? => Not ready. if not hyperion_client: raise ConfigEntryNotReady version = await hyperion_client.async_sysinfo_version() @@ -110,6 +123,31 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except ValueError: pass + # Client needs authentication, but no token provided? => Reauth. + auth_resp = await hyperion_client.async_is_auth_required() + if ( + auth_resp is not None + and client.ResponseOK(auth_resp) + and auth_resp.get(hyperion_const.KEY_INFO, {}).get( + hyperion_const.KEY_REQUIRED, False + ) + and token is None + ): + await _create_reauth_flow(hass, config_entry) + return False + + # Client login doesn't work? => Reauth. + if not await hyperion_client.async_client_login(): + await _create_reauth_flow(hass, config_entry) + return False + + # Cannot switch instance or cannot load state? => Not ready. + if ( + not await hyperion_client.async_client_switch_instance() + or not client.ServerInfoResponseOK(await hyperion_client.async_get_serverinfo()) + ): + raise ConfigEntryNotReady + hyperion_client.set_callbacks( { f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: ( @@ -139,17 +177,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ] ) hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( - config_entry.add_update_listener(_async_options_updated) + config_entry.add_update_listener(_async_entry_updated) ) hass.async_create_task(setup_then_listen()) return True -async def _async_options_updated( +async def _async_entry_updated( hass: HomeAssistantType, config_entry: ConfigEntry ) -> None: - """Handle options update.""" + """Handle entry updates.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index aef74e530b..11ab3289d1 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -12,11 +12,19 @@ import voluptuous as vol from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( CONN_CLASS_LOCAL_PUSH, + SOURCE_REAUTH, ConfigEntry, ConfigFlow, OptionsFlow, ) -from homeassistant.const import CONF_BASE, CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN +from homeassistant.const import ( + CONF_BASE, + CONF_HOST, + CONF_ID, + CONF_PORT, + CONF_SOURCE, + CONF_TOKEN, +) from homeassistant.core import callback from homeassistant.helpers.typing import ConfigType @@ -35,13 +43,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) -# +------------------+ +------------------+ +--------------------+ -# |Step: SSDP | |Step: user | |Step: import | -# | | | | | | -# |Input: | |Input: | |Input: | -# +------------------+ +------------------+ +--------------------+ -# v v v -# +----------------------+-----------------------+ +# +------------------+ +------------------+ +--------------------+ +--------------------+ +# |Step: SSDP | |Step: user | |Step: import | |Step: reauth | +# | | | | | | | | +# |Input: | |Input: | |Input: | |Input: | +# +------------------+ +------------------+ +--------------------+ +--------------------+ +# v v v v +# +-------------------+-----------------------+--------------------+ # Auth not | Auth | # required? | required? | # | v @@ -82,7 +90,7 @@ _LOGGER.setLevel(logging.DEBUG) # | # v # +----------------+ -# | Create! | +# | Create/Update! | # +----------------+ # A note on choice of discovery mechanisms: Hyperion supports both Zeroconf and SSDP out @@ -140,6 +148,17 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) + async def async_step_reauth( + self, + config_data: ConfigType, + ) -> Dict[str, Any]: + """Handle a reauthentication flow.""" + self._data = dict(config_data) + async with self._create_client(raw_connection=True) as hyperion_client: + if not hyperion_client: + return self.async_abort(reason="cannot_connect") + return await self._advance_to_auth_step_if_necessary(hyperion_client) + async def async_step_ssdp( # type: ignore[override] self, discovery_info: Dict[str, Any] ) -> Dict[str, Any]: @@ -401,7 +420,18 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): if not hyperion_id: return self.async_abort(reason="no_id") - await self.async_set_unique_id(hyperion_id, raise_on_progress=False) + entry = await self.async_set_unique_id(hyperion_id, raise_on_progress=False) + + # pylint: disable=no-member + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and entry is not None: + assert self.hass + self.hass.config_entries.async_update_entry(entry, data=self._data) + # Need to manually reload, as the listener won't have been installed because + # the initial load did not succeed (the reauth flow will not be initiated if + # the load succeeds) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + self._abort_if_unique_id_configured() # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 9875f3bd91..6d61af0b66 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -16,8 +16,6 @@ CONF_ON_UNLOAD = "ON_UNLOAD" SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}" SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}" -SOURCE_IMPORT = "import" - HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 5aa087c051..e2989cb973 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, LightEntity, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -47,7 +47,6 @@ from .const import ( DOMAIN, SIGNAL_INSTANCE_REMOVED, SIGNAL_INSTANCES_UPDATED, - SOURCE_IMPORT, TYPE_HYPERION_LIGHT, ) diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json index 180f266f1a..ca7ed238f0 100644 --- a/homeassistant/components/hyperion/strings.json +++ b/homeassistant/components/hyperion/strings.json @@ -37,7 +37,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI", "auth_new_token_not_work_error": "Failed to authenticate using newly created token", - "no_id": "The Hyperion Ambilight instance did not report its id" + "no_id": "The Hyperion Ambilight instance did not report its id", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index a2febcca2a..31a6c49eeb 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -50,6 +50,20 @@ TEST_INSTANCE_3: Dict[str, Any] = { "running": True, } +TEST_AUTH_REQUIRED_RESP: Dict[str, Any] = { + "command": "authorize-tokenRequired", + "info": { + "required": True, + }, + "success": True, + "tan": 1, +} + +TEST_AUTH_NOT_REQUIRED_RESP = { + **TEST_AUTH_REQUIRED_RESP, + "info": {"required": False}, +} + _LOGGER = logging.getLogger(__name__) @@ -78,12 +92,7 @@ def create_mock_client() -> Mock: mock_client.async_client_connect = AsyncMock(return_value=True) mock_client.async_client_disconnect = AsyncMock(return_value=True) mock_client.async_is_auth_required = AsyncMock( - return_value={ - "command": "authorize-tokenRequired", - "info": {"required": False}, - "success": True, - "tan": 1, - } + return_value=TEST_AUTH_NOT_REQUIRED_RESP ) mock_client.async_login = AsyncMock( return_value={"command": "authorize-login", "success": True, "tan": 0} @@ -91,6 +100,17 @@ def create_mock_client() -> Mock: mock_client.async_sysinfo_id = AsyncMock(return_value=TEST_SYSINFO_ID) mock_client.async_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_ID) + mock_client.async_client_switch_instance = AsyncMock(return_value=True) + mock_client.async_client_login = AsyncMock(return_value=True) + mock_client.async_get_serverinfo = AsyncMock( + return_value={ + "command": "serverinfo", + "success": True, + "tan": 0, + "info": {"fake": "data"}, + } + ) + mock_client.adjustment = None mock_client.effects = None mock_client.instances = [ @@ -100,12 +120,15 @@ def create_mock_client() -> Mock: return mock_client -def add_test_config_entry(hass: HomeAssistantType) -> ConfigEntry: +def add_test_config_entry( + hass: HomeAssistantType, data: Optional[Dict[str, Any]] = None +) -> ConfigEntry: """Add a test config entry.""" config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] entry_id=TEST_CONFIG_ENTRY_ID, domain=DOMAIN, - data={ + data=data + or { CONF_HOST: TEST_HOST, CONF_PORT: TEST_PORT, }, @@ -118,10 +141,12 @@ def add_test_config_entry(hass: HomeAssistantType) -> ConfigEntry: async def setup_test_config_entry( - hass: HomeAssistantType, hyperion_client: Optional[Mock] = None + hass: HomeAssistantType, + config_entry: Optional[ConfigEntry] = None, + hyperion_client: Optional[Mock] = None, ) -> ConfigEntry: """Add a test Hyperion entity to hass.""" - config_entry = add_test_config_entry(hass) + config_entry = config_entry or add_test_config_entry(hass) hyperion_client = hyperion_client or create_mock_client() # pylint: disable=attribute-defined-outside-init diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 807a3829e7..481b795784 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -11,10 +11,14 @@ from homeassistant.components.hyperion.const import ( CONF_CREATE_TOKEN, CONF_PRIORITY, DOMAIN, - SOURCE_IMPORT, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -25,6 +29,7 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from . import ( + TEST_AUTH_REQUIRED_RESP, TEST_CONFIG_ENTRY_ID, TEST_ENTITY_ID_1, TEST_HOST, @@ -49,15 +54,6 @@ TEST_HOST_PORT: Dict[str, Any] = { CONF_PORT: TEST_PORT, } -TEST_AUTH_REQUIRED_RESP = { - "command": "authorize-tokenRequired", - "info": { - "required": True, - }, - "success": True, - "tan": 1, -} - TEST_AUTH_ID = "ABCDE" TEST_REQUEST_TOKEN_SUCCESS = { "command": "authorize-requestToken", @@ -694,3 +690,62 @@ async def test_options(hass: HomeAssistantType) -> None: blocking=True, ) assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority + + +async def test_reauth_success(hass: HomeAssistantType) -> None: + """Check a reauth flow that succeeds.""" + + config_data = { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + } + + config_entry = add_test_config_entry(hass, data=config_data) + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch("homeassistant.components.hyperion.async_setup", return_value=True), patch( + "homeassistant.components.hyperion.async_setup_entry", return_value=True + ): + result = await _init_flow( + hass, + source=SOURCE_REAUTH, + data=config_data, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert CONF_TOKEN in config_entry.data + + +async def test_reauth_cannot_connect(hass: HomeAssistantType) -> None: + """Check a reauth flow that fails to connect.""" + + config_data = { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + } + + add_test_config_entry(hass, data=config_data) + client = create_mock_client() + client.async_client_connect = AsyncMock(return_value=False) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow( + hass, + source=SOURCE_REAUTH, + data=config_data, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 5366f6e14d..4636a9ad59 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -17,12 +17,26 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.config_entries import ( + ENTRY_STATE_SETUP_ERROR, + SOURCE_REAUTH, + ConfigEntry, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_PORT, + CONF_SOURCE, + CONF_TOKEN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import HomeAssistantType from . import ( + TEST_AUTH_NOT_REQUIRED_RESP, + TEST_AUTH_REQUIRED_RESP, TEST_CONFIG_ENTRY_OPTIONS, TEST_ENTITY_ID_1, TEST_ENTITY_ID_2, @@ -206,7 +220,9 @@ async def test_setup_config_entry(hass: HomeAssistantType) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None -async def test_setup_config_entry_not_ready(hass: HomeAssistantType) -> None: +async def test_setup_config_entry_not_ready_connect_fail( + hass: HomeAssistantType, +) -> None: """Test the component not being ready.""" client = create_mock_client() client.async_client_connect = AsyncMock(return_value=False) @@ -214,6 +230,32 @@ async def test_setup_config_entry_not_ready(hass: HomeAssistantType) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is None +async def test_setup_config_entry_not_ready_switch_instance_fail( + hass: HomeAssistantType, +) -> None: + """Test the component not being ready.""" + client = create_mock_client() + client.async_client_switch_instance = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is None + + +async def test_setup_config_entry_not_ready_load_state_fail( + hass: HomeAssistantType, +) -> None: + """Test the component not being ready.""" + client = create_mock_client() + client.async_get_serverinfo = AsyncMock( + return_value={ + "command": "serverinfo", + "success": False, + } + ) + + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is None + + async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None: """Test dynamic changes in the omstamce configuration.""" config_entry = add_test_config_entry(hass) @@ -724,7 +766,7 @@ async def test_unload_entry(hass: HomeAssistantType) -> None: client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) assert hass.states.get(TEST_ENTITY_ID_1) is not None - assert client.async_client_connect.called + assert client.async_client_connect.call_count == 2 assert not client.async_client_disconnect.called entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) assert entry @@ -749,3 +791,44 @@ async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: await setup_test_config_entry(hass, hyperion_client=client) assert hass.states.get(TEST_ENTITY_ID_1) is not None assert "Please consider upgrading" not in caplog.text + + +async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: + """Verify a reauth flow when auth is required but no token provided.""" + client = create_mock_client() + config_entry = add_test_config_entry(hass) + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + mock_flow_init.assert_called_once_with( + DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data=config_entry.data, + ) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + +async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: + """Verify a reauth flow when a bad token is provided.""" + client = create_mock_client() + config_entry = add_test_config_entry( + hass, + data={CONF_HOST: TEST_HOST, CONF_PORT: TEST_PORT, CONF_TOKEN: "expired_token"}, + ) + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_NOT_REQUIRED_RESP) + + # Fail to log in. + client.async_client_login = AsyncMock(return_value=False) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + mock_flow_init.assert_called_once_with( + DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data=config_entry.data, + ) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR From 735607c6258f8eeaa3fb691ea9b896b7755640ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Dec 2020 23:59:45 +0100 Subject: [PATCH 146/302] Bump version to 2021.1 (#44298) --- homeassistant/const.py | 4 ++-- tests/components/cloud/test_account_link.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 469c0fa7fb..de18fd4ed0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 0 -MINOR_VERSION = 119 +MAJOR_VERSION = 2021 +MINOR_VERSION = 1 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index ce310001b3..1580969b0a 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -44,7 +44,7 @@ async def test_setup_provide_implementation(hass): "homeassistant.components.cloud.account_link._get_services", return_value=[ {"service": "test", "min_version": "0.1.0"}, - {"service": "too_new", "min_version": "100.0.0"}, + {"service": "too_new", "min_version": "1000000.0.0"}, ], ): assert ( From 638374c36acf82c18ca8fb34c49b29bce43a180f Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Wed, 16 Dec 2020 23:01:39 +0000 Subject: [PATCH 147/302] Bump pyroon to 0.0.28 (#44302) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 4f5601a7f3..4bd5903253 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", "requirements": [ - "roonapi==0.0.25" + "roonapi==0.0.28" ], "codeowners": [ "@pavoni" diff --git a/requirements_all.txt b/requirements_all.txt index 20e3c4c115..489b4198be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1958,7 +1958,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.25 +roonapi==0.0.28 # homeassistant.components.rova rova==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27fe91d0eb..a4150cbfcd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -963,7 +963,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.25 +roonapi==0.0.28 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From d3255e63e3ed5e9209ce3f6a1dc15937e55cefe5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 17 Dec 2020 00:02:51 +0000 Subject: [PATCH 148/302] [ci skip] Translation update --- .../components/apple_tv/translations/it.json | 2 +- .../components/gios/translations/it.json | 2 +- .../components/hyperion/translations/en.json | 3 ++- .../components/neato/translations/de.json | 24 +++++++++---------- .../components/neato/translations/en.json | 18 ++++++++++++-- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index 3a4ae887aa..7ed3306721 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -30,7 +30,7 @@ "data": { "pin": "Codice PIN" }, - "description": "L'abbinamento \u00e8 richiesto per il {protocol} \"{protocol}\". Immettere il codice PIN visualizzato sullo schermo. Gli zeri iniziali devono essere omessi, ovvero immettere 123 se il codice visualizzato \u00e8 0123.", + "description": "L'abbinamento \u00e8 richiesto per il protocollo \"{protocol}\". Immettere il codice PIN visualizzato sullo schermo. Gli zeri iniziali devono essere omessi, ovvero immettere 123 se il codice visualizzato \u00e8 0123.", "title": "Abbinamento" }, "reconfigure": { diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index 5d1e99d17f..26bf8386d6 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Server GIO\u015a raggiungibile" + "can_reach_server": "Raggiungi il server GIO\u015a" } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/en.json b/homeassistant/components/hyperion/translations/en.json index c4c4f512d6..d1277b411e 100644 --- a/homeassistant/components/hyperion/translations/en.json +++ b/homeassistant/components/hyperion/translations/en.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Failed to authenticate using newly created token", "auth_required_error": "Failed to determine if authorization is required", "cannot_connect": "Failed to connect", - "no_id": "The Hyperion Ambilight instance did not report its id" + "no_id": "The Hyperion Ambilight instance did not report its id", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index c8dcc93500..c41d4e6d93 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -1,23 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert.", - "authorize_url_timeout": "Timeout beim Erzeugen der Autorisierungs-URL.", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachten Sie die Dokumentation.", - "no_url_available": "Keine URL verfügbar. Informationen zu diesem Fehler finden Sie [im Hilfebereich]({docs_url})", - "reauth_successful": "Re-Authentifizierung war erfolgreich" + "already_configured": "Bereits konfiguriert" }, "create_entry": { - "default": "Erfolgreich authentifiziert" + "default": "Siehe [Neato-Dokumentation]({docs_url})." }, "step": { - "pick_implementation": { - "title": "Authentifizierungsmethode auswählen" - }, - "reauth_confirm": { - "title": "Einrichtung bestätigen?" + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "vendor": "Hersteller" + }, + "description": "Siehe [Neato-Dokumentation]({docs_url}).", + "title": "Neato-Kontoinformationen" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json index 333c8a980f..cc63397964 100644 --- a/homeassistant/components/neato/translations/en.json +++ b/homeassistant/components/neato/translations/en.json @@ -1,8 +1,9 @@ { "config": { "abort": { - "already_configured": "Account is already configured.", + "already_configured": "Device is already configured", "authorize_url_timeout": "Timeout generating authorize URL.", + "invalid_auth": "Invalid authentication", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful" @@ -10,12 +11,25 @@ "create_entry": { "default": "Successfully authenticated" }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, "step": { "pick_implementation": { "title": "Pick Authentication Method" }, "reauth_confirm": { - "title": "Confirm setup?" + "title": "Do you want to start set up?" + }, + "user": { + "data": { + "password": "Password", + "username": "Username", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url}).", + "title": "Neato Account Info" } } }, From 63dfd8343d34927de161a5f3525d6f78507b8045 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 17 Dec 2020 13:20:18 +0100 Subject: [PATCH 149/302] Increase surepetcare api timeout to 60s (#44316) --- homeassistant/components/surepetcare/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 7f0213be4e..165e0bfd98 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -24,7 +24,7 @@ SURE_IDS = "sure_ids" TOPIC_UPDATE = f"{DOMAIN}_data_update" # sure petcare api -SURE_API_TIMEOUT = 15 +SURE_API_TIMEOUT = 60 # flap BATTERY_ICON = "mdi:battery" From 7c63119ad2045fc9ed3ed1840bec09e7de4e61cb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 17 Dec 2020 16:44:24 +0100 Subject: [PATCH 150/302] Fix philips_js channel and source name entry (#44296) --- homeassistant/components/philips_js/media_player.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 451e8d6a3c..7ccec14406 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -313,10 +313,11 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): self._tv.update() self._sources = { - srcid: source["name"] or f"Source {srcid}" + srcid: source.get("name") or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } self._channels = { - chid: channel["name"] for chid, channel in (self._tv.channels or {}).items() + chid: channel.get("name") or f"Channel {chid}" + for chid, channel in (self._tv.channels or {}).items() } From 94e1f8e6315387f00cd770cbc759f464e5d5fe13 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 17 Dec 2020 16:44:46 +0100 Subject: [PATCH 151/302] Refactor Airly tests (#44315) --- tests/components/airly/__init__.py | 29 +++---- tests/components/airly/test_air_quality.py | 76 +++++++++--------- tests/components/airly/test_config_flow.py | 93 ++++++++++------------ tests/components/airly/test_init.py | 84 +++++++++---------- tests/components/airly/test_sensor.py | 74 +++++++++-------- 5 files changed, 171 insertions(+), 185 deletions(-) diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 29828bddc1..87e549b929 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -1,32 +1,33 @@ """Tests for Airly.""" -import json - from homeassistant.components.airly.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture +API_KEY_VALIDATION_URL = ( + "https://airapi.airly.eu/v2/measurements/point?lat=52.241310&lng=20.991010" +) +API_POINT_URL = ( + "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" +) -async def init_integration(hass, forecast=False) -> MockConfigEntry: + +async def init_integration(hass, aioclient_mock) -> MockConfigEntry: """Set up the Airly integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id="55.55-122.12", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/airly/test_air_quality.py b/tests/components/airly/test_air_quality.py index fca2761f2f..24a98cbf15 100644 --- a/tests/components/airly/test_air_quality.py +++ b/tests/components/airly/test_air_quality.py @@ -1,6 +1,5 @@ """Test air_quality of Airly integration.""" from datetime import timedelta -import json from airly.exceptions import AirlyError @@ -21,19 +20,21 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + HTTP_INTERNAL_SERVER_ERROR, STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch +from . import API_POINT_URL + from tests.common import async_fire_time_changed, load_fixture from tests.components.airly import init_integration -async def test_air_quality(hass): +async def test_air_quality(hass, aioclient_mock): """Test states of the air_quality.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) registry = await hass.helpers.entity_registry.async_get_registry() state = hass.states.get("air_quality.home") @@ -58,56 +59,55 @@ async def test_air_quality(hass): entry = registry.async_get("air_quality.home") assert entry - assert entry.unique_id == "55.55-122.12" + assert entry.unique_id == "123-456" -async def test_availability(hass): +async def test_availability(hass, aioclient_mock): """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) state = hass.states.get("air_quality.home") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "14" + aioclient_mock.clear_requests() + aioclient_mock.get( + API_POINT_URL, exc=AirlyError(HTTP_INTERNAL_SERVER_ERROR, "Unexpected error") + ) future = utcnow() + timedelta(minutes=60) - with patch( - "airly._private._RequestsHandler.get", - side_effect=AirlyError(500, "Unexpected error"), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("air_quality.home") - assert state - assert state.state == STATE_UNAVAILABLE + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get("air_quality.home") + assert state + assert state.state == STATE_UNAVAILABLE + + aioclient_mock.clear_requests() + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) future = utcnow() + timedelta(minutes=120) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("air_quality.home") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "14" + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "14" -async def test_manual_update_entity(hass): +async def test_manual_update_entity(hass, aioclient_mock): """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) + call_count = aioclient_mock.call_count await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.components.airly.AirlyDataUpdateCoordinator._async_update_data" - ) as mock_update: - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["air_quality.home"]}, - blocking=True, - ) - assert mock_update.call_count == 1 + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["air_quality.home"]}, + blocking=True, + ) + + assert aioclient_mock.call_count == call_count + 1 diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index d7d45bbd7e..e5ae80022b 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -1,6 +1,4 @@ """Define tests for the Airly config flow.""" -import json - from airly.exceptions import AirlyError from homeassistant import data_entry_flow @@ -11,14 +9,15 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - HTTP_FORBIDDEN, + HTTP_UNAUTHORIZED, ) -from tests.async_mock import patch -from tests.common import MockConfigEntry, load_fixture +from . import API_KEY_VALIDATION_URL, API_POINT_URL + +from tests.common import MockConfigEntry, load_fixture, patch CONFIG = { - CONF_NAME: "abcd", + CONF_NAME: "Home", CONF_API_KEY: "foo", CONF_LATITUDE: 123, CONF_LONGITUDE: 456, @@ -35,69 +34,63 @@ async def test_show_form(hass): assert result["step_id"] == SOURCE_USER -async def test_invalid_api_key(hass): +async def test_invalid_api_key(hass, aioclient_mock): """Test that errors are shown when API key is invalid.""" - with patch( - "airly._private._RequestsHandler.get", - side_effect=AirlyError( - HTTP_FORBIDDEN, {"message": "Invalid authentication credentials"} + aioclient_mock.get( + API_KEY_VALIDATION_URL, + exc=AirlyError( + HTTP_UNAUTHORIZED, {"message": "Invalid authentication credentials"} ), - ): + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - assert result["errors"] == {"base": "invalid_api_key"} + assert result["errors"] == {"base": "invalid_api_key"} -async def test_invalid_location(hass): +async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_no_station.json")), - ): + aioclient_mock.get( + API_KEY_VALIDATION_URL, text=load_fixture("airly_valid_station.json") + ) + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - assert result["errors"] == {"base": "wrong_location"} + assert result["errors"] == {"base": "wrong_location"} -async def test_duplicate_error(hass): +async def test_duplicate_error(hass, aioclient_mock): """Test that errors are shown when duplicates are added.""" + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + MockConfigEntry(domain=DOMAIN, unique_id="123-456", data=CONFIG).add_to_hass(hass) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - MockConfigEntry(domain=DOMAIN, unique_id="123-456", data=CONFIG).add_to_hass( - hass - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result["type"] == "abort" + assert result["reason"] == "already_configured" -async def test_create_entry(hass): +async def test_create_entry(hass, aioclient_mock): """Test that the user step works.""" + aioclient_mock.get( + API_KEY_VALIDATION_URL, text=load_fixture("airly_valid_station.json") + ) + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONFIG[CONF_NAME] - assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] - assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] - assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index 28f2aca4fb..cb0ccf268f 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,6 +1,5 @@ """Test init of Airly integration.""" from datetime import timedelta -import json from homeassistant.components.airly.const import DOMAIN from homeassistant.config_entries import ( @@ -10,14 +9,15 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import patch +from . import API_POINT_URL + from tests.common import MockConfigEntry, load_fixture from tests.components.airly import init_integration -async def test_async_setup_entry(hass): +async def test_async_setup_entry(hass, aioclient_mock): """Test a successful setup entry.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) state = hass.states.get("air_quality.home") assert state is not None @@ -25,75 +25,69 @@ async def test_async_setup_entry(hass): assert state.state == "14" -async def test_config_not_ready(hass): +async def test_config_not_ready(hass, aioclient_mock): """Test for setup failure if connection to Airly is missing.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id="55.55-122.12", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch("airly._private._RequestsHandler.get", side_effect=ConnectionError()): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_config_without_unique_id(hass): +async def test_config_without_unique_id(hass, aioclient_mock): """Test for setup entry without unique_id.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED - assert entry.unique_id == "55.55-122.12" + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_LOADED + assert entry.unique_id == "123-456" -async def test_config_with_turned_off_station(hass): +async def test_config_with_turned_off_station(hass, aioclient_mock): """Test for setup entry for a turned off measuring station.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id="55.55-122.12", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_no_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_update_interval(hass): +async def test_update_interval(hass, aioclient_mock): """Test correct update interval when the number of configured instances changes.""" - entry = await init_integration(hass) + entry = await init_integration(hass, aioclient_mock) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -112,13 +106,13 @@ async def test_update_interval(hass): }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + aioclient_mock.get( + "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", + text=load_fixture("airly_valid_station.json"), + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert entry.state == ENTRY_STATE_LOADED @@ -126,9 +120,9 @@ async def test_update_interval(hass): assert instance.update_interval == timedelta(minutes=30) -async def test_unload_entry(hass): +async def test_unload_entry(hass, aioclient_mock): """Test successful unload of entry.""" - entry = await init_integration(hass) + entry = await init_integration(hass, aioclient_mock) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 45b98d7c27..abc53294bb 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -1,6 +1,5 @@ """Test sensor of Airly integration.""" from datetime import timedelta -import json from homeassistant.components.airly.sensor import ATTRIBUTION from homeassistant.const import ( @@ -21,14 +20,15 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch +from . import API_POINT_URL + from tests.common import async_fire_time_changed, load_fixture from tests.components.airly import init_integration -async def test_sensor(hass): +async def test_sensor(hass, aioclient_mock): """Test states of the sensor.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) registry = await hass.helpers.entity_registry.async_get_registry() state = hass.states.get("sensor.home_humidity") @@ -40,7 +40,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_humidity") assert entry - assert entry.unique_id == "55.55-122.12-humidity" + assert entry.unique_id == "123-456-humidity" state = hass.states.get("sensor.home_pm1") assert state @@ -54,7 +54,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_pm1") assert entry - assert entry.unique_id == "55.55-122.12-pm1" + assert entry.unique_id == "123-456-pm1" state = hass.states.get("sensor.home_pressure") assert state @@ -65,7 +65,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_pressure") assert entry - assert entry.unique_id == "55.55-122.12-pressure" + assert entry.unique_id == "123-456-pressure" state = hass.states.get("sensor.home_temperature") assert state @@ -76,53 +76,51 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_temperature") assert entry - assert entry.unique_id == "55.55-122.12-temperature" + assert entry.unique_id == "123-456-temperature" -async def test_availability(hass): +async def test_availability(hass, aioclient_mock): """Ensure that we mark the entities unavailable correctly when service is offline.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) state = hass.states.get("sensor.home_humidity") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "92.8" + aioclient_mock.clear_requests() + aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) future = utcnow() + timedelta(minutes=60) - with patch("airly._private._RequestsHandler.get", side_effect=ConnectionError()): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = hass.states.get("sensor.home_humidity") - assert state - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state == STATE_UNAVAILABLE + aioclient_mock.clear_requests() + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) future = utcnow() + timedelta(minutes=120) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = hass.states.get("sensor.home_humidity") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "92.8" + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "92.8" -async def test_manual_update_entity(hass): +async def test_manual_update_entity(hass, aioclient_mock): """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) + call_count = aioclient_mock.call_count await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.components.airly.AirlyDataUpdateCoordinator._async_update_data" - ) as mock_update: - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["sensor.home_humidity"]}, - blocking=True, - ) - assert mock_update.call_count == 1 + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.home_humidity"]}, + blocking=True, + ) + + assert aioclient_mock.call_count == call_count + 1 From 524db33f13541c0fe12adece411f7f1d99aa404b Mon Sep 17 00:00:00 2001 From: Thibaut Date: Thu, 17 Dec 2020 16:46:22 +0100 Subject: [PATCH 152/302] Add Somfy battery sensor (#44311) Co-authored-by: Martin Hjelmare --- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/somfy/climate.py | 42 ++-------------- homeassistant/components/somfy/sensor.py | 58 ++++++++++++++++++++++ 3 files changed, 63 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/somfy/sensor.py diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index a831b55606..78429dd1fe 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -47,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["cover", "switch", "climate"] +SOMFY_COMPONENTS = ["climate", "cover", "sensor", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index 49b528645e..c4abc12d24 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -1,7 +1,6 @@ """Support for Somfy Thermostat.""" -import logging -from typing import Any, Dict, List, Optional +from typing import List, Optional from pymfy.api.devices.category import Category from pymfy.api.devices.thermostat import ( @@ -14,9 +13,6 @@ from pymfy.api.devices.thermostat import ( from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -26,13 +22,11 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import SomfyEntity from .const import API, COORDINATOR, DOMAIN -_LOGGER = logging.getLogger(__name__) - SUPPORTED_CATEGORIES = {Category.HVAC.value} PRESET_FROST_GUARD = "Frost Guard" @@ -88,11 +82,6 @@ class SomfyClimate(SomfyEntity, ClimateEntity): """Return the list of supported features.""" return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the state attributes of the device.""" - return {ATTR_BATTERY_LEVEL: self._climate.get_battery()} - @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" @@ -145,13 +134,11 @@ class SomfyClimate(SomfyEntity, ClimateEntity): HEAT and COOL mode are exclusive. End user has to enable a mode manually within the Somfy application. So only one mode can be displayed. Auto mode is a scheduler. """ - hvac_state = HVAC_MODES_MAPPING.get(self._climate.get_hvac_state()) + hvac_state = HVAC_MODES_MAPPING[self._climate.get_hvac_state()] return [HVAC_MODE_AUTO, hvac_state] def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if hvac_mode == self.hvac_mode: - return if hvac_mode == HVAC_MODE_AUTO: self._climate.cancel_target() else: @@ -159,26 +146,6 @@ class SomfyClimate(SomfyEntity, ClimateEntity): TargetMode.MANUAL, self.target_temperature, DurationType.FURTHER_NOTICE ) - @property - def hvac_action(self) -> str: - """Return the current running hvac operation if supported.""" - if not self.current_temperature or not self.target_temperature: - return CURRENT_HVAC_IDLE - - if ( - self.hvac_mode == HVAC_MODE_HEAT - and self.current_temperature < self.target_temperature - ): - return CURRENT_HVAC_HEAT - - if ( - self.hvac_mode == HVAC_MODE_COOL - and self.current_temperature > self.target_temperature - ): - return CURRENT_HVAC_COOL - - return CURRENT_HVAC_IDLE - @property def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" @@ -206,8 +173,7 @@ class SomfyClimate(SomfyEntity, ClimateEntity): elif preset_mode in [PRESET_MANUAL, PRESET_GEOFENCING]: temperature = self.target_temperature else: - _LOGGER.error("Preset mode not supported: %s", preset_mode) - return + raise ValueError(f"Preset mode not supported: {preset_mode}") self._climate.set_target( REVERSE_PRESET_MAPPING[preset_mode], temperature, DurationType.NEXT_MODE diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py new file mode 100644 index 0000000000..3d7b1b8fc1 --- /dev/null +++ b/homeassistant/components/somfy/sensor.py @@ -0,0 +1,58 @@ +"""Support for Somfy Thermostat Battery.""" + +from pymfy.api.devices.category import Category +from pymfy.api.devices.thermostat import Thermostat + +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE + +from . import SomfyEntity +from .const import API, COORDINATOR, DOMAIN + +SUPPORTED_CATEGORIES = {Category.HVAC.value} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy climate platform.""" + + def get_thermostats(): + """Retrieve thermostats.""" + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] + + return [ + SomfyThermostatBatterySensor(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] + + async_add_entities(await hass.async_add_executor_job(get_thermostats)) + + +class SomfyThermostatBatterySensor(SomfyEntity): + """Representation of a Somfy thermostat battery.""" + + def __init__(self, coordinator, device_id, api): + """Initialize the Somfy device.""" + super().__init__(coordinator, device_id, api) + self._climate = None + self._create_device() + + def _create_device(self): + """Update the device with the latest data.""" + self._climate = Thermostat(self.device, self.api) + + @property + def state(self) -> int: + """Return the state of the sensor.""" + return self._climate.get_battery() + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of the sensor.""" + return PERCENTAGE From fe7109acf42d75ef634c8781723d7b4414f90089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 17 Dec 2020 16:47:20 +0100 Subject: [PATCH 153/302] Add extended device info and some attributes to Apple TV (#44277) --- homeassistant/components/apple_tv/__init__.py | 33 +++++++++++----- .../components/apple_tv/media_player.py | 38 ++++++++++++++++++- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 14170fdd8c..eca5e91dde 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -135,15 +136,6 @@ class AppleTVEntity(Entity): def async_device_disconnected(self): """Handle when connection was lost to device.""" - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._identifier)}, - "manufacturer": "Apple", - "name": self.name, - } - @property def name(self): """Return the name of the device.""" @@ -337,6 +329,8 @@ class AppleTVManager: self._dispatch_send(SIGNAL_CONNECTED, self.atv) self._address_updated(str(conf.address)) + await self._async_setup_device_registry() + self._connection_attempts = 0 if self._connection_was_lost: _LOGGER.info( @@ -344,6 +338,27 @@ class AppleTVManager: ) self._connection_was_lost = False + async def _async_setup_device_registry(self): + attrs = { + "identifiers": {(DOMAIN, self.config_entry.unique_id)}, + "manufacturer": "Apple", + "name": self.config_entry.data[CONF_NAME], + } + + if self.atv: + dev_info = self.atv.device_info + + attrs["model"] = "Apple TV " + dev_info.model.name.replace("Gen", "") + attrs["sw_version"] = dev_info.version + + if dev_info.mac: + attrs["connections"] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} + + device_registry = await dr.async_get_registry(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, **attrs + ) + @property def is_connecting(self): """Return true if connection is in progress.""" diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index b7486af50e..81bb79dc50 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,7 +1,7 @@ """Support for Apple TV media player.""" import logging -from pyatv.const import DeviceState, MediaType +from pyatv.const import DeviceState, FeatureName, FeatureState, MediaType from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -107,6 +107,22 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self._playing = None self.async_write_ha_state() + @property + def app_id(self): + """ID of the current running app.""" + if self.atv: + if self.atv.features.in_state(FeatureState.Available, FeatureName.App): + return self.atv.metadata.app.identifier + return None + + @property + def app_name(self): + """Name of the current running app.""" + if self.atv: + if self.atv.features.in_state(FeatureState.Available, FeatureName.App): + return self.atv.metadata.app.name + return None + @property def media_content_type(self): """Content type of current playing media.""" @@ -168,11 +184,31 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self._playing.title return None + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + if self._is_feature_available(FeatureName.Artist): + return self._playing.artist + return None + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + if self._is_feature_available(FeatureName.Album): + return self._playing.album + return None + @property def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_APPLE_TV + def _is_feature_available(self, feature): + """Return if a feature is available.""" + if self.atv and self._playing: + return self.atv.features.in_state(FeatureState.Available, feature) + return False + async def async_turn_on(self): """Turn the media player on.""" await self.manager.connect() From c8e3b17362f842649327fda40c13a2133df0bd0f Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 17 Dec 2020 08:01:22 -0800 Subject: [PATCH 154/302] Update quality_scale for Hyperion (#44306) --- homeassistant/components/hyperion/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index d8c6a2c352..7d6c6222d2 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -6,6 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/hyperion", "domain": "hyperion", "name": "Hyperion", + "quality_scale": "platinum", "requirements": [ "hyperion-py==0.6.0" ], From 2b6842aee07c950bf8d6b115aa6a61d4184cc7fa Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 17 Dec 2020 16:12:06 +0000 Subject: [PATCH 155/302] Fix velux homekit covers not enumerated correctly (#44318) --- .../components/homekit_controller/cover.py | 1 + .../specific_devices/test_velux_gateway.py | 79 ++++ .../homekit_controller/velux_gateway.json | 380 ++++++++++++++++++ 3 files changed, 460 insertions(+) create mode 100644 tests/components/homekit_controller/specific_devices/test_velux_gateway.py create mode 100644 tests/fixtures/homekit_controller/velux_gateway.json diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index fdf48ebba5..6c945c8111 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -248,4 +248,5 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity): ENTITY_TYPES = { ServicesTypes.GARAGE_DOOR_OPENER: HomeKitGarageDoorCover, ServicesTypes.WINDOW_COVERING: HomeKitWindowCover, + ServicesTypes.WINDOW: HomeKitWindowCover, } diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py new file mode 100644 index 0000000000..033b4aa7b4 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -0,0 +1,79 @@ +""" +Test against characteristics captured from a Velux Gateway. + +https://github.com/home-assistant/core/issues/44314 +""" + +from homeassistant.components.cover import ( + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, +) + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_simpleconnect_cover_setup(hass): + """Test that a velux gateway can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "velux_gateway.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the cover is correctly found and set up + cover_id = "cover.velux_window" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-1111111a114a111a-8" + + cover_helper = Helper( + hass, + cover_id, + pairing, + accessories[0], + config_entry, + ) + + cover_state = await cover_helper.poll_and_get_state() + assert cover_state.attributes["friendly_name"] == "VELUX Window" + assert cover_state.state == "closed" + assert cover_state.attributes["supported_features"] == ( + SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_OPEN + ) + + # Check that one of the sensors is correctly found and set up + sensor_id = "sensor.velux_sensor_temperature" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-a11b111-8" + + sensor_helper = Helper( + hass, + sensor_id, + pairing, + accessories[0], + config_entry, + ) + + sensor_state = await sensor_helper.poll_and_get_state() + assert sensor_state.attributes["friendly_name"] == "VELUX Sensor Temperature" + assert sensor_state.state == "18.9" + + # The cover and sensor are different devices (accessories) attached to the same bridge + assert cover.device_id != sensor.device_id + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(cover.device_id) + assert device.manufacturer == "VELUX" + assert device.name == "VELUX Window" + assert device.model == "VELUX Window" + assert device.sw_version == "48" + + bridge = device_registry.async_get(device.via_device_id) + assert bridge.manufacturer == "VELUX" + assert bridge.name == "VELUX Gateway" + assert bridge.model == "VELUX Gateway" + assert bridge.sw_version == "70" diff --git a/tests/fixtures/homekit_controller/velux_gateway.json b/tests/fixtures/homekit_controller/velux_gateway.json new file mode 100644 index 0000000000..1a6f60537b --- /dev/null +++ b/tests/fixtures/homekit_controller/velux_gateway.json @@ -0,0 +1,380 @@ +[ + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "a1a11a1" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pr" + ], + "format": "string", + "value": "70" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "1.1.0" + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 2, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "a11b111" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pr" + ], + "format": "string", + "value": "16" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "Temperature sensor" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 18.9, + "minValue": 0, + "maxValue": 50, + "minStep": 0.1, + "unit": "celsius" + } + ], + "hidden": false, + "primary": true + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 11, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": [ + "pr" + ], + "format": "string", + "value": "Humidity sensor" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 58, + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "00000097-0000-1000-8000-0026BB765291", + "iid": 14, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": [ + "pr" + ], + "format": "string", + "value": "Carbon Dioxide sensor" + }, + { + "type": "00000092-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 1, + "minValue": 0, + "minStep": 1 + }, + { + "type": "00000093-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 400, + "minValue": 0, + "maxValue": 5000 + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 3, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "1111111a114a111a" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pr" + ], + "format": "string", + "value": "48" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008B-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "Roof Window" + }, + { + "type": "0000007C-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "0000006D-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "00000072-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 2, + "maxValue": 2, + "minValue": 0, + "minStep": 1 + } + ], + "hidden": false, + "primary": true + } + ] + } +] \ No newline at end of file From 31b806ced114256c923279f22b7625448e0a0309 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Dec 2020 17:43:52 +0100 Subject: [PATCH 156/302] Revert "Change http to auto for cast media image url" (#44327) --- homeassistant/components/cast/media_player.py | 4 +- tests/components/cast/test_media_player.py | 44 ------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b76dbcaf20..e68800efb4 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -665,9 +665,7 @@ class CastDevice(MediaPlayerEntity): images = media_status.images - return ( - images[0].url.replace("http://", "//") if images and images[0].url else None - ) + return images[0].url if images and images[0].url else None @property def media_image_remotely_accessible(self) -> bool: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 0e9f6c13e3..4f75e93fae 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -787,50 +787,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == "unknown" -async def test_url_replace(hass: HomeAssistantType): - """Test functionality of replacing URL for HTTPS.""" - entity_id = "media_player.speaker" - reg = await hass.helpers.entity_registry.async_get_registry() - - info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) - - chromecast = await async_setup_media_player_cast(hass, info) - _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) - - connection_status = MagicMock() - connection_status.status = "CONNECTED" - conn_status_cb(connection_status) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.name == "Speaker" - assert state.state == "unknown" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) - - class FakeHTTPImage: - url = "http://example.com/test.png" - - class FakeHTTPSImage: - url = "https://example.com/test.png" - - media_status = MagicMock(images=[FakeHTTPImage()]) - media_status.player_is_playing = True - media_status_cb(media_status) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.attributes.get("entity_picture") == "//example.com/test.png" - - media_status.images = [FakeHTTPSImage()] - media_status_cb(media_status) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.attributes.get("entity_picture") == "https://example.com/test.png" - - async def test_group_media_states(hass, mz_mock): """Test media states are read from group if entity has no state.""" entity_id = "media_player.speaker" From 418304c702d6aee9ede885f9c1fe5e43a2e3f453 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 17 Dec 2020 11:08:19 -0700 Subject: [PATCH 157/302] Bump pyMyQ to version 2.0.12 (#44328) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index ee3471725b..5f863ad7f3 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.11"], + "requirements": ["pymyq==2.0.12"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 489b4198be..ba1a755176 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.11 +pymyq==2.0.12 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4150cbfcd..992b3d0816 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.11 +pymyq==2.0.12 # homeassistant.components.nut pynut2==2.1.2 From 97894bd718a11218d00dc05f860a8ea233e0b325 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 17 Dec 2020 19:34:40 +0100 Subject: [PATCH 158/302] Refactor Airly config flow (#44330) --- homeassistant/components/airly/config_flow.py | 63 +++++++++---------- tests/components/airly/__init__.py | 3 - tests/components/airly/test_config_flow.py | 10 +-- 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 8b3b1949ec..f745c75689 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -5,7 +5,13 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -37,23 +43,24 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" ) self._abort_if_unique_id_configured() - api_key_valid = await self._test_api_key(websession, user_input["api_key"]) - if not api_key_valid: - self._errors["base"] = "invalid_api_key" - else: - location_valid = await self._test_location( + try: + location_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], user_input["longitude"], ) + except AirlyError as err: + if err.status_code == HTTP_UNAUTHORIZED: + self._errors["base"] = "invalid_api_key" + else: if not location_valid: self._errors["base"] = "wrong_location" - if not self._errors: - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + if not self._errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) return self._show_config_form( name=DEFAULT_NAME, @@ -81,31 +88,19 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def _test_api_key(self, client, api_key): - """Return true if api_key is valid.""" - with async_timeout.timeout(10): - airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=52.24131, longitude=20.99101 - ) - try: - await measurements.update() - except AirlyError: - return False - return True +async def test_location(client, api_key, latitude, longitude): + """Return true if location is valid.""" + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) - async def _test_location(self, client, api_key, latitude, longitude): - """Return true if location is valid.""" + with async_timeout.timeout(10): + await measurements.update() - with async_timeout.timeout(10): - airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=latitude, longitude=longitude - ) + current = measurements.current - await measurements.update() - current = measurements.current - if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: - return False - return True + if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: + return False + return True diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 87e549b929..197864b807 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -3,9 +3,6 @@ from homeassistant.components.airly.const import DOMAIN from tests.common import MockConfigEntry, load_fixture -API_KEY_VALIDATION_URL = ( - "https://airapi.airly.eu/v2/measurements/point?lat=52.241310&lng=20.991010" -) API_POINT_URL = ( "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" ) diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index e5ae80022b..46dc5510b1 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) -from . import API_KEY_VALIDATION_URL, API_POINT_URL +from . import API_POINT_URL from tests.common import MockConfigEntry, load_fixture, patch @@ -37,7 +37,7 @@ async def test_show_form(hass): async def test_invalid_api_key(hass, aioclient_mock): """Test that errors are shown when API key is invalid.""" aioclient_mock.get( - API_KEY_VALIDATION_URL, + API_POINT_URL, exc=AirlyError( HTTP_UNAUTHORIZED, {"message": "Invalid authentication credentials"} ), @@ -52,9 +52,6 @@ async def test_invalid_api_key(hass, aioclient_mock): async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" - aioclient_mock.get( - API_KEY_VALIDATION_URL, text=load_fixture("airly_valid_station.json") - ) aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) result = await hass.config_entries.flow.async_init( @@ -79,9 +76,6 @@ async def test_duplicate_error(hass, aioclient_mock): async def test_create_entry(hass, aioclient_mock): """Test that the user step works.""" - aioclient_mock.get( - API_KEY_VALIDATION_URL, text=load_fixture("airly_valid_station.json") - ) aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) with patch("homeassistant.components.airly.async_setup_entry", return_value=True): From d18c9f1c74c61a5fbb2d8d1eb80830788f9fdb42 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 17 Dec 2020 12:59:45 -0700 Subject: [PATCH 159/302] Update ReCollect docs to use proper name (#44291) --- .../components/recollect_waste/__init__.py | 4 ++-- .../components/recollect_waste/config_flow.py | 4 ++-- homeassistant/components/recollect_waste/const.py | 2 +- homeassistant/components/recollect_waste/sensor.py | 14 +++++++------- .../components/recollect_waste/test_config_flow.py | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 57bd346c91..10d39c1fc4 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,4 +1,4 @@ -"""The Recollect Waste integration.""" +"""The ReCollect Waste integration.""" import asyncio from datetime import date, timedelta from typing import List @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) except RecollectError as err: raise UpdateFailed( - f"Error while requesting data from Recollect: {err}" + f"Error while requesting data from ReCollect: {err}" ) from err coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index f0d1527a0f..402c143706 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Recollect Waste integration.""" +"""Config flow for ReCollect Waste integration.""" from aiorecollect.client import Client from aiorecollect.errors import RecollectError import voluptuous as vol @@ -19,7 +19,7 @@ DATA_SCHEMA = vol.Schema( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Recollect Waste.""" + """Handle a config flow for ReCollect Waste.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL diff --git a/homeassistant/components/recollect_waste/const.py b/homeassistant/components/recollect_waste/const.py index 8012bdbb02..4a6c9dbda6 100644 --- a/homeassistant/components/recollect_waste/const.py +++ b/homeassistant/components/recollect_waste/const.py @@ -1,4 +1,4 @@ -"""Define Recollect Waste constants.""" +"""Define ReCollect Waste constants.""" import logging DOMAIN = "recollect_waste" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 53304c9321..405d66989b 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,4 +1,4 @@ -"""Support for Recollect Waste sensors.""" +"""Support for ReCollect Waste sensors.""" from typing import Callable import voluptuous as vol @@ -20,7 +20,7 @@ ATTR_AREA_NAME = "area_name" ATTR_NEXT_PICKUP_TYPES = "next_pickup_types" ATTR_NEXT_PICKUP_DATE = "next_pickup_date" -DEFAULT_ATTRIBUTION = "Pickup data provided by Recollect Waste" +DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "recollect_waste" DEFAULT_ICON = "mdi:trash-can-outline" @@ -43,7 +43,7 @@ async def async_setup_platform( ): """Import Awair configuration from YAML.""" LOGGER.warning( - "Loading Recollect Waste via platform setup is deprecated. " + "Loading ReCollect Waste via platform setup is deprecated. " "Please remove it from your configuration." ) hass.async_create_task( @@ -58,13 +58,13 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable ) -> None: - """Set up Recollect Waste sensors based on a config entry.""" + """Set up ReCollect Waste sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] - async_add_entities([RecollectWasteSensor(coordinator, entry)]) + async_add_entities([ReCollectWasteSensor(coordinator, entry)]) -class RecollectWasteSensor(CoordinatorEntity): - """Recollect Waste Sensor.""" +class ReCollectWasteSensor(CoordinatorEntity): + """ReCollect Waste Sensor.""" def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index bec87b72ee..1f17e1f60d 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,4 +1,4 @@ -"""Define tests for the Recollect Waste config flow.""" +"""Define tests for the ReCollect Waste config flow.""" from aiorecollect.errors import RecollectError from homeassistant import data_entry_flow From 6ffa3c18b2f1ccbfd142ee0f8689c98668350a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Thu, 17 Dec 2020 21:09:58 +0100 Subject: [PATCH 160/302] Upgrade Telegram lib, refactor component for breaking changes (#44147) Co-authored-by: Martin Hjelmare --- .../components/telegram_bot/__init__.py | 162 ++++++++++++++---- .../components/telegram_bot/manifest.json | 2 +- .../components/telegram_bot/polling.py | 25 +-- .../components/telegram_bot/services.yaml | 3 + requirements_all.txt | 2 +- 5 files changed, 147 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index a39b6b300d..b6ca788161 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -553,7 +553,7 @@ class TelegramNotificationService: ) return params - def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg): + def _send_msg(self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg): """Send one message.""" try: @@ -572,7 +572,6 @@ class TelegramNotificationService: ATTR_CHAT_ID: chat_id, ATTR_MESSAGEID: message_id, } - message_tag = kwargs_msg.get(ATTR_MESSAGE_TAG) if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data) @@ -594,7 +593,17 @@ class TelegramNotificationService: for chat_id in self._get_target_chat_ids(target): _LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params) self._send_msg( - self.bot.sendMessage, "Error sending message", chat_id, text, **params + self.bot.send_message, + "Error sending message", + params[ATTR_MESSAGE_TAG], + chat_id, + text, + parse_mode=params[ATTR_PARSER], + disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_to_message_id=params[ATTR_REPLY_TO_MSGID], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], ) def delete_message(self, chat_id=None, **kwargs): @@ -603,7 +612,7 @@ class TelegramNotificationService: message_id, _ = self._get_msg_ids(kwargs, chat_id) _LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id) deleted = self._send_msg( - self.bot.deleteMessage, "Error deleting message", chat_id, message_id + self.bot.delete_message, "Error deleting message", None, chat_id, message_id ) # reduce message_id anyway: if self._last_message_id[chat_id] is not None: @@ -628,26 +637,41 @@ class TelegramNotificationService: text = f"{title}\n{message}" if title else message _LOGGER.debug("Editing message with ID %s", message_id or inline_message_id) return self._send_msg( - self.bot.editMessageText, + self.bot.edit_message_text, "Error editing text message", + params[ATTR_MESSAGE_TAG], text, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, - **params, + parse_mode=params[ATTR_PARSER], + disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], ) if type_edit == SERVICE_EDIT_CAPTION: - func_send = self.bot.editMessageCaption - params[ATTR_CAPTION] = kwargs.get(ATTR_CAPTION) - else: - func_send = self.bot.editMessageReplyMarkup + return self._send_msg( + self.bot.edit_message_caption, + "Error editing message attributes", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id, + caption=kwargs.get(ATTR_CAPTION), + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + return self._send_msg( - func_send, + self.bot.edit_message_reply_markup, "Error editing message attributes", + params[ATTR_MESSAGE_TAG], chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, - **params, + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], ) def answer_callback_query( @@ -662,26 +686,18 @@ class TelegramNotificationService: show_alert, ) self._send_msg( - self.bot.answerCallbackQuery, + self.bot.answer_callback_query, "Error sending answer callback query", + params[ATTR_MESSAGE_TAG], callback_query_id, text=message, show_alert=show_alert, - **params, + timeout=params[ATTR_TIMEOUT], ) def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): """Send a photo, sticker, video, or document.""" params = self._get_msg_kwargs(kwargs) - caption = kwargs.get(ATTR_CAPTION) - func_send = { - SERVICE_SEND_PHOTO: self.bot.sendPhoto, - SERVICE_SEND_STICKER: self.bot.sendSticker, - SERVICE_SEND_ANIMATION: self.bot.sendAnimation, - SERVICE_SEND_VIDEO: self.bot.sendVideo, - SERVICE_SEND_VOICE: self.bot.sendVoice, - SERVICE_SEND_DOCUMENT: self.bot.sendDocument, - }.get(file_type) file_content = load_data( self.hass, url=kwargs.get(ATTR_URL), @@ -691,17 +707,89 @@ class TelegramNotificationService: authentication=kwargs.get(ATTR_AUTHENTICATION), verify_ssl=kwargs.get(ATTR_VERIFY_SSL), ) + if file_content: for chat_id in self._get_target_chat_ids(target): - _LOGGER.debug("Send file to chat ID %s. Caption: %s", chat_id, caption) - self._send_msg( - func_send, - "Error sending file", - chat_id, - file_content, - caption=caption, - **params, - ) + _LOGGER.debug("Sending file to chat ID %s", chat_id) + + if file_type == SERVICE_SEND_PHOTO: + self._send_msg( + self.bot.send_photo, + "Error sending photo", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + photo=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + + elif file_type == SERVICE_SEND_STICKER: + self._send_msg( + self.bot.send_sticker, + "Error sending sticker", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + sticker=file_content, + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + ) + + elif file_type == SERVICE_SEND_VIDEO: + self._send_msg( + self.bot.send_video, + "Error sending video", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + video=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + elif file_type == SERVICE_SEND_DOCUMENT: + self._send_msg( + self.bot.send_document, + "Error sending document", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + document=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + elif file_type == SERVICE_SEND_VOICE: + self._send_msg( + self.bot.send_voice, + "Error sending voice", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + voice=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + ) + elif file_type == SERVICE_SEND_ANIMATION: + self._send_msg( + self.bot.send_animation, + "Error sending animation", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + animation=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + file_content.seek(0) else: _LOGGER.error("Can't send file with kwargs: %s", kwargs) @@ -716,19 +804,23 @@ class TelegramNotificationService: "Send location %s/%s to chat ID %s", latitude, longitude, chat_id ) self._send_msg( - self.bot.sendLocation, + self.bot.send_location, "Error sending location", + params[ATTR_MESSAGE_TAG], chat_id=chat_id, latitude=latitude, longitude=longitude, - **params, + disable_notification=params[ATTR_DISABLE_NOTIF], + timeout=params[ATTR_TIMEOUT], ) def leave_chat(self, chat_id=None): """Remove bot from chat.""" chat_id = self._get_target_chat_ids(chat_id)[0] _LOGGER.debug("Leave from chat ID %s", chat_id) - leaved = self._send_msg(self.bot.leaveChat, "Error leaving chat", chat_id) + leaved = self._send_msg( + self.bot.leave_chat, "Error leaving chat", None, chat_id + ) return leaved diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index 29f6ade8af..80d9b50932 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -2,7 +2,7 @@ "domain": "telegram_bot", "name": "Telegram bot", "documentation": "https://www.home-assistant.io/integrations/telegram_bot", - "requirements": ["python-telegram-bot==11.1.0", "PySocks==1.7.1"], + "requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"], "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 8bdeef2511..b617826411 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -3,10 +3,10 @@ import logging from telegram import Update from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut -from telegram.ext import Handler, Updater +from telegram.ext import CallbackContext, Dispatcher, Handler, Updater +from telegram.utils.types import HandlerArg from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback from . import CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, initialize_bot @@ -18,12 +18,10 @@ async def async_setup_platform(hass, config): bot = initialize_bot(config) pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS]) - @callback def _start_bot(_event): """Start the bot.""" pol.start_polling() - @callback def _stop_bot(_event): """Stop the bot.""" pol.stop_polling() @@ -34,15 +32,15 @@ async def async_setup_platform(hass, config): return True -def process_error(bot, update, error): +def process_error(update: Update, context: CallbackContext): """Telegram bot error handler.""" try: - raise error + raise context.error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass except TelegramError: - _LOGGER.error('Update "%s" caused error "%s"', update, error) + _LOGGER.error('Update "%s" caused error: "%s"', update, context.error) def message_handler(handler): @@ -59,10 +57,17 @@ def message_handler(handler): """Check is update valid.""" return isinstance(update, Update) - def handle_update(self, update, dispatcher): + def handle_update( + self, + update: HandlerArg, + dispatcher: Dispatcher, + check_result: object, + context: CallbackContext = None, + ): """Handle update.""" optional_args = self.collect_optional_args(dispatcher, update) - return self.callback(dispatcher.bot, update, **optional_args) + context.args = optional_args + return self.callback(update, context) return MessageHandler() @@ -89,6 +94,6 @@ class TelegramPoll(BaseTelegramBotEntity): """Stop the polling task.""" self.updater.stop() - def process_update(self, bot, update): + def process_update(self, update: HandlerArg, context: CallbackContext): """Process incoming message.""" self.process_message(update.to_dict()) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 0560e6541b..5e2b06564d 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -374,6 +374,9 @@ answer_callback_query: show_alert: description: Show a permanent notification. example: true + timeout: + description: Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc) + example: "1000" delete_message: description: Delete a previously sent message. diff --git a/requirements_all.txt b/requirements_all.txt index ba1a755176..3a04884562 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1816,7 +1816,7 @@ python-songpal==0.12 python-tado==0.8.1 # homeassistant.components.telegram_bot -python-telegram-bot==11.1.0 +python-telegram-bot==13.1 # homeassistant.components.vlc_telnet python-telnet-vlc==1.0.4 From f54fcb76468e316e48bdf44fa7aa3f949efa770a Mon Sep 17 00:00:00 2001 From: mbo18 Date: Thu, 17 Dec 2020 22:04:20 +0100 Subject: [PATCH 161/302] Add new sensors to meteo_france (#44150) Co-authored-by: Thibaut --- .../components/meteo_france/const.py | 24 +++++++++++++++++++ .../components/meteo_france/sensor.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 0055d32393..d642d3c6e0 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -85,6 +85,14 @@ SENSOR_TYPES = { ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:freezing", }, + "wind_gust": { + ENTITY_NAME: "Wind gust", + ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, + ENTITY_ICON: "mdi:weather-windy-variant", + ENTITY_DEVICE_CLASS: None, + ENTITY_ENABLE: False, + ENTITY_API_DATA_PATH: "current_forecast:wind:gust", + }, "wind_speed": { ENTITY_NAME: "Wind speed", ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, @@ -141,6 +149,22 @@ SENSOR_TYPES = { ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "current_forecast:clouds", }, + "original_condition": { + ENTITY_NAME: "Original condition", + ENTITY_UNIT: None, + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: None, + ENTITY_ENABLE: False, + ENTITY_API_DATA_PATH: "current_forecast:weather:desc", + }, + "daily_original_condition": { + ENTITY_NAME: "Daily original condition", + ENTITY_UNIT: None, + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: None, + ENTITY_ENABLE: False, + ENTITY_API_DATA_PATH: "today_forecast:weather12H:desc", + }, } CONDITION_CLASSES = { diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 3c88914aaf..00f3c2da2c 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -115,7 +115,7 @@ class MeteoFranceSensor(CoordinatorEntity): else: value = data[path[1]] - if self._type == "wind_speed": + if self._type in ["wind_speed", "wind_gust"]: # convert API wind speed from m/s to km/h value = round(value * 3.6) return value From c5fdde2a943bf15adff982e8b53db97ea3b88fa9 Mon Sep 17 00:00:00 2001 From: Christopher Gramberg Date: Thu, 17 Dec 2020 15:08:35 -0600 Subject: [PATCH 162/302] Convert filter tests to use pytest style (#41743) --- tests/common.py | 15 +- tests/components/filter/test_sensor.py | 533 ++++++++++++------------- 2 files changed, 280 insertions(+), 268 deletions(-) diff --git a/tests/common.py b/tests/common.py index 66303ad96b..ce07f5ab61 100644 --- a/tests/common.py +++ b/tests/common.py @@ -54,7 +54,7 @@ from homeassistant.helpers import ( storage, ) from homeassistant.helpers.json import JSONEncoder -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -801,6 +801,19 @@ def init_recorder_component(hass, add_config=None): _LOGGER.info("In-memory recorder successfully started") +async def async_init_recorder_component(hass, add_config=None): + """Initialize the recorder asynchronously.""" + config = dict(add_config) if add_config else {} + config[recorder.CONF_DB_URL] = "sqlite://" + + with patch("homeassistant.components.recorder.migration.migrate_schema"): + assert await async_setup_component( + hass, recorder.DOMAIN, {recorder.DOMAIN: config} + ) + assert recorder.DOMAIN in hass.config.components + _LOGGER.info("In-memory recorder successfully started") + + def mock_restore_cache(hass, states): """Mock the DATA_RESTORE_CACHE.""" key = restore_state.DATA_RESTORE_STATE_TASK diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 0aa390223c..454fcc976f 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,7 +1,8 @@ """The test for the data filter sensor platform.""" from datetime import timedelta from os import path -import unittest + +from pytest import fixture from homeassistant import config as hass_config from homeassistant.components.filter.sensor import ( @@ -15,309 +16,307 @@ from homeassistant.components.filter.sensor import ( ) from homeassistant.const import SERVICE_RELOAD import homeassistant.core as ha -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.async_mock import patch -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - init_recorder_component, -) +from tests.common import assert_setup_component, async_init_recorder_component -class TestFilterSensor(unittest.TestCase): - """Test the Data Filter sensor.""" +@fixture +def values(): + """Fixture for a list of test States.""" + values = [] + raw_values = [20, 19, 18, 21, 22, 0] + timestamp = dt_util.utcnow() + for val in raw_values: + values.append(ha.State("sensor.test_monitored", val, last_updated=timestamp)) + timestamp += timedelta(minutes=1) + return values - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.components.add("history") - raw_values = [20, 19, 18, 21, 22, 0] - self.values = [] - timestamp = dt_util.utcnow() - for val in raw_values: - self.values.append( - ha.State("sensor.test_monitored", val, last_updated=timestamp) - ) - timestamp += timedelta(minutes=1) +async def init_recorder(hass): + """Init the recorder for testing.""" + await async_init_recorder_component(hass) + await hass.async_start() - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() - def init_recorder(self): - """Initialize the recorder.""" - init_recorder_component(self.hass) - self.hass.start() - - def test_setup_fail(self): - """Test if filter doesn't exist.""" - config = { - "sensor": { - "platform": "filter", - "entity_id": "sensor.test_monitored", - "filters": [{"filter": "nonexisting"}], - } +async def test_setup_fail(hass): + """Test if filter doesn't exist.""" + config = { + "sensor": { + "platform": "filter", + "entity_id": "sensor.test_monitored", + "filters": [{"filter": "nonexisting"}], } - with assert_setup_component(0): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + } + hass.config.components.add("history") + with assert_setup_component(0): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - def test_chain(self): - """Test if filter chaining works.""" - config = { - "sensor": { - "platform": "filter", - "name": "test", - "entity_id": "sensor.test_monitored", - "filters": [ - {"filter": "outlier", "window_size": 10, "radius": 4.0}, - {"filter": "lowpass", "time_constant": 10, "precision": 2}, - {"filter": "throttle", "window_size": 1}, - ], - } + +async def test_chain(hass, values): + """Test if filter chaining works.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], } + } + hass.config.components.add("history") + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - with assert_setup_component(1, "sensor"): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + for value in values: + hass.states.async_set(config["sensor"]["entity_id"], value.state) + await hass.async_block_till_done() - for value in self.values: - self.hass.states.set(config["sensor"]["entity_id"], value.state) - self.hass.block_till_done() + state = hass.states.get("sensor.test") + assert "18.05" == state.state - state = self.hass.states.get("sensor.test") - assert "18.05" == state.state - def test_chain_history(self, missing=False): - """Test if filter chaining works.""" - self.init_recorder() - config = { - "history": {}, - "sensor": { - "platform": "filter", - "name": "test", - "entity_id": "sensor.test_monitored", - "filters": [ - {"filter": "outlier", "window_size": 10, "radius": 4.0}, - {"filter": "lowpass", "time_constant": 10, "precision": 2}, - {"filter": "throttle", "window_size": 1}, - ], - }, - } - t_0 = dt_util.utcnow() - timedelta(minutes=1) - t_1 = dt_util.utcnow() - timedelta(minutes=2) - t_2 = dt_util.utcnow() - timedelta(minutes=3) - t_3 = dt_util.utcnow() - timedelta(minutes=4) - - if missing: - fake_states = {} - else: - fake_states = { - "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", "unknown", last_changed=t_1), - ha.State("sensor.test_monitored", 19.0, last_changed=t_2), - ha.State("sensor.test_monitored", 18.2, last_changed=t_3), - ] - } - - with patch( - "homeassistant.components.history.state_changes_during_period", - return_value=fake_states, - ): - with patch( - "homeassistant.components.history.get_last_state_changes", - return_value=fake_states, - ): - with assert_setup_component(1, "sensor"): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set(config["sensor"]["entity_id"], value.state) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - if missing: - assert "18.05" == state.state - else: - assert "17.05" == state.state - - def test_chain_history_missing(self): - """Test if filter chaining works when recorder is enabled but the source is not recorded.""" - return self.test_chain_history(missing=True) - - def test_history_time(self): - """Test loading from history based on a time window.""" - self.init_recorder() - config = { - "history": {}, - "sensor": { - "platform": "filter", - "name": "test", - "entity_id": "sensor.test_monitored", - "filters": [{"filter": "time_throttle", "window_size": "00:01"}], - }, - } - t_0 = dt_util.utcnow() - timedelta(minutes=1) - t_1 = dt_util.utcnow() - timedelta(minutes=2) - t_2 = dt_util.utcnow() - timedelta(minutes=3) +async def test_chain_history(hass, values, missing=False): + """Test if filter chaining works.""" + await init_recorder(hass) + config = { + "history": {}, + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], + }, + } + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) + t_3 = dt_util.utcnow() - timedelta(minutes=4) + if missing: + fake_states = {} + else: fake_states = { "sensor.test_monitored": [ ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", 19.0, last_changed=t_1), - ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ha.State("sensor.test_monitored", "unknown", last_changed=t_1), + ha.State("sensor.test_monitored", 19.0, last_changed=t_2), + ha.State("sensor.test_monitored", 18.2, last_changed=t_3), ] } + + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ): with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.history.get_last_state_changes", return_value=fake_states, ): - with patch( - "homeassistant.components.history.get_last_state_changes", - return_value=fake_states, - ): - with assert_setup_component(1, "sensor"): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - self.hass.block_till_done() - state = self.hass.states.get("sensor.test") - assert "18.0" == state.state + for value in values: + hass.states.async_set(config["sensor"]["entity_id"], value.state) + await hass.async_block_till_done() - def test_outlier(self): - """Test if outlier filter works.""" - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - for state in self.values: - filtered = filt.filter_state(state) - assert 21 == filtered.state - - def test_outlier_step(self): - """ - Test step-change handling in outlier. - - Test if outlier filter handles long-running step-changes correctly. - It should converge to no longer filter once just over half the - window_size is occupied by the new post step-change values. - """ - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=1.1) - self.values[-1].state = 22 - for state in self.values: - filtered = filt.filter_state(state) - assert 22 == filtered.state - - def test_initial_outlier(self): - """Test issue #13363.""" - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", 4000) - for state in [out] + self.values: - filtered = filt.filter_state(state) - assert 21 == filtered.state - - def test_unknown_state_outlier(self): - """Test issue #32395.""" - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", "unknown") - for state in [out] + self.values + [out]: - try: - filtered = filt.filter_state(state) - except ValueError: - assert state.state == "unknown" - assert 21 == filtered.state - - def test_precision_zero(self): - """Test if precision of zero returns an integer.""" - filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) - for state in self.values: - filtered = filt.filter_state(state) - assert isinstance(filtered.state, int) - - def test_lowpass(self): - """Test if lowpass filter works.""" - filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) - out = ha.State("sensor.test_monitored", "unknown") - for state in [out] + self.values + [out]: - try: - filtered = filt.filter_state(state) - except ValueError: - assert state.state == "unknown" - assert 18.05 == filtered.state - - def test_range(self): - """Test if range filter works.""" - lower = 10 - upper = 20 - filt = RangeFilter( - entity=None, precision=2, lower_bound=lower, upper_bound=upper - ) - for unf_state in self.values: - unf = float(unf_state.state) - filtered = filt.filter_state(unf_state) - if unf < lower: - assert lower == filtered.state - elif unf > upper: - assert upper == filtered.state + state = hass.states.get("sensor.test") + if missing: + assert "18.05" == state.state else: - assert unf == filtered.state + assert "17.05" == state.state - def test_range_zero(self): - """Test if range filter works with zeroes as bounds.""" - lower = 0 - upper = 0 - filt = RangeFilter( - entity=None, precision=2, lower_bound=lower, upper_bound=upper - ) - for unf_state in self.values: - unf = float(unf_state.state) - filtered = filt.filter_state(unf_state) - if unf < lower: - assert lower == filtered.state - elif unf > upper: - assert upper == filtered.state - else: - assert unf == filtered.state - def test_throttle(self): - """Test if lowpass filter works.""" - filt = ThrottleFilter(window_size=3, precision=2, entity=None) - filtered = [] - for state in self.values: - new_state = filt.filter_state(state) - if not filt.skip_processing: - filtered.append(new_state) - assert [20, 21] == [f.state for f in filtered] +async def test_chain_history_missing(hass, values): + """Test if filter chaining works when recorder is enabled but the source is not recorded.""" + await test_chain_history(hass, values, missing=True) - def test_time_throttle(self): - """Test if lowpass filter works.""" - filt = TimeThrottleFilter( - window_size=timedelta(minutes=2), precision=2, entity=None - ) - filtered = [] - for state in self.values: - new_state = filt.filter_state(state) - if not filt.skip_processing: - filtered.append(new_state) - assert [20, 18, 22] == [f.state for f in filtered] - def test_time_sma(self): - """Test if time_sma filter works.""" - filt = TimeSMAFilter( - window_size=timedelta(minutes=2), precision=2, entity=None, type="last" - ) - for state in self.values: +async def test_history_time(hass): + """Test loading from history based on a time window.""" + await init_recorder(hass) + config = { + "history": {}, + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [{"filter": "time_throttle", "window_size": "00:01"}], + }, + } + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) + + fake_states = { + "sensor.test_monitored": [ + ha.State("sensor.test_monitored", 18.0, last_changed=t_0), + ha.State("sensor.test_monitored", 19.0, last_changed=t_1), + ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ] + } + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ): + with patch( + "homeassistant.components.history.get_last_state_changes", + return_value=fake_states, + ): + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert "18.0" == state.state + + +async def test_outlier(values): + """Test if outlier filter works.""" + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) + for state in values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + +def test_outlier_step(values): + """ + Test step-change handling in outlier. + + Test if outlier filter handles long-running step-changes correctly. + It should converge to no longer filter once just over half the + window_size is occupied by the new post step-change values. + """ + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=1.1) + values[-1].state = 22 + for state in values: + filtered = filt.filter_state(state) + assert 22 == filtered.state + + +def test_initial_outlier(values): + """Test issue #13363.""" + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) + out = ha.State("sensor.test_monitored", 4000) + for state in [out] + values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + +def test_unknown_state_outlier(values): + """Test issue #32395.""" + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) + out = ha.State("sensor.test_monitored", "unknown") + for state in [out] + values + [out]: + try: filtered = filt.filter_state(state) - assert 21.5 == filtered.state + except ValueError: + assert state.state == "unknown" + assert 21 == filtered.state + + +def test_precision_zero(values): + """Test if precision of zero returns an integer.""" + filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) + for state in values: + filtered = filt.filter_state(state) + assert isinstance(filtered.state, int) + + +def test_lowpass(values): + """Test if lowpass filter works.""" + filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) + out = ha.State("sensor.test_monitored", "unknown") + for state in [out] + values + [out]: + try: + filtered = filt.filter_state(state) + except ValueError: + assert state.state == "unknown" + assert 18.05 == filtered.state + + +def test_range(values): + """Test if range filter works.""" + lower = 10 + upper = 20 + filt = RangeFilter(entity=None, precision=2, lower_bound=lower, upper_bound=upper) + for unf_state in values: + unf = float(unf_state.state) + filtered = filt.filter_state(unf_state) + if unf < lower: + assert lower == filtered.state + elif unf > upper: + assert upper == filtered.state + else: + assert unf == filtered.state + + +def test_range_zero(values): + """Test if range filter works with zeroes as bounds.""" + lower = 0 + upper = 0 + filt = RangeFilter(entity=None, precision=2, lower_bound=lower, upper_bound=upper) + for unf_state in values: + unf = float(unf_state.state) + filtered = filt.filter_state(unf_state) + if unf < lower: + assert lower == filtered.state + elif unf > upper: + assert upper == filtered.state + else: + assert unf == filtered.state + + +def test_throttle(values): + """Test if lowpass filter works.""" + filt = ThrottleFilter(window_size=3, precision=2, entity=None) + filtered = [] + for state in values: + new_state = filt.filter_state(state) + if not filt.skip_processing: + filtered.append(new_state) + assert [20, 21] == [f.state for f in filtered] + + +def test_time_throttle(values): + """Test if lowpass filter works.""" + filt = TimeThrottleFilter( + window_size=timedelta(minutes=2), precision=2, entity=None + ) + filtered = [] + for state in values: + new_state = filt.filter_state(state) + if not filt.skip_processing: + filtered.append(new_state) + assert [20, 18, 22] == [f.state for f in filtered] + + +def test_time_sma(values): + """Test if time_sma filter works.""" + filt = TimeSMAFilter( + window_size=timedelta(minutes=2), precision=2, entity=None, type="last" + ) + for state in values: + filtered = filt.filter_state(state) + assert 21.5 == filtered.state async def test_reload(hass): """Verify we can reload filter sensors.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db + await init_recorder(hass) hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component( From 4bdb793a94f508c9081cb5c51d353361b5fee1e1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 18 Dec 2020 00:03:48 +0000 Subject: [PATCH 163/302] [ci skip] Translation update --- .../components/abode/translations/de.json | 18 ++++++- .../accuweather/translations/de.json | 5 ++ .../accuweather/translations/lb.json | 3 +- .../components/apple_tv/translations/de.json | 45 +++++++++++++++- .../components/apple_tv/translations/lb.json | 48 ++++++++++++++++- .../flick_electric/translations/de.json | 1 + .../components/hyperion/translations/cs.json | 3 +- .../components/hyperion/translations/de.json | 22 ++++++++ .../components/hyperion/translations/et.json | 3 +- .../components/hyperion/translations/it.json | 3 +- .../components/hyperion/translations/no.json | 3 +- .../components/hyperion/translations/ru.json | 3 +- .../hyperion/translations/zh-Hant.json | 3 +- .../components/ipma/translations/de.json | 5 ++ .../components/isy994/translations/de.json | 13 +++-- .../components/kulersky/translations/de.json | 13 +++++ .../lutron_caseta/translations/de.json | 1 + .../mobile_app/translations/de.json | 5 ++ .../components/neato/translations/cs.json | 17 +++++-- .../components/neato/translations/de.json | 22 ++++++-- .../components/neato/translations/et.json | 17 +++++-- .../components/neato/translations/it.json | 15 +++++- .../components/neato/translations/no.json | 17 +++++-- .../components/neato/translations/ru.json | 17 +++++-- .../neato/translations/zh-Hant.json | 17 +++++-- .../components/nest/translations/de.json | 5 +- .../components/nws/translations/cs.json | 2 +- .../components/ozw/translations/de.json | 7 +++ .../components/poolsense/translations/de.json | 3 +- .../components/tuya/translations/de.json | 5 +- .../components/tuya/translations/zh-Hans.json | 51 ++++++++++++++++++- .../components/zerproc/translations/de.json | 3 +- 32 files changed, 355 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/hyperion/translations/de.json create mode 100644 homeassistant/components/kulersky/translations/de.json diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json index a5d3686886..43d6ba21ca 100644 --- a/homeassistant/components/abode/translations/de.json +++ b/homeassistant/components/abode/translations/de.json @@ -1,13 +1,27 @@ { "config": { "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_mfa_code": "Ung\u00fcltiger MFA-Code" + }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-Code (6-stellig)" + }, + "title": "Gib deinen MFA-Code f\u00fcr Abode ein" + }, "reauth_confirm": { "data": { - "password": "Passwort" - } + "password": "Passwort", + "username": "E-Mail" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" }, "user": { "data": { diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 9291e17e86..1dd547b6fc 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -10,5 +10,10 @@ "title": "AccuWeather" } } + }, + "system_health": { + "info": { + "remaining_requests": "Verbleibende erlaubte Anfragen" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/lb.json b/homeassistant/components/accuweather/translations/lb.json index 4d9689ccf5..7f3855a7b9 100644 --- a/homeassistant/components/accuweather/translations/lb.json +++ b/homeassistant/components/accuweather/translations/lb.json @@ -34,7 +34,8 @@ }, "system_health": { "info": { - "can_reach_server": "AccuWeather Server ereechbar" + "can_reach_server": "AccuWeather Server ereechbar", + "remaining_requests": "Rescht vun erlaabten Ufroen" } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index b1ed434e21..4c72601942 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -1,11 +1,54 @@ { "config": { + "abort": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "backoff": "Ger\u00e4t akzeptiert derzeit keine Kopplungsanfragen (Sie haben m\u00f6glicherweise zu oft einen ung\u00fcltigen PIN-Code eingegeben), versuchen Sie es sp\u00e4ter erneut.", + "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", + "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuchen Sie, es erneut hinzuzuf\u00fcgen.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "unknown": "Unerwarteter Fehler" + }, "flow_title": "Apple TV: {name}", "step": { + "pair_no_pin": { + "title": "Kopplung" + }, "pair_with_pin": { "data": { "pin": "PIN-Code" - } + }, + "title": "Kopplung" + }, + "reconfigure": { + "description": "Dieses Apple TV hat Verbindungsprobleme und muss neu konfiguriert werden.", + "title": "Ger\u00e4teneukonfiguration" + }, + "service_problem": { + "description": "Beim Koppeln des Protokolls `{protocol}` ist ein Problem aufgetreten. Es wird ignoriert.", + "title": "Fehler beim Hinzuf\u00fcgen des Dienstes" + }, + "user": { + "data": { + "device_input": "Ger\u00e4t" + }, + "title": "Einrichten eines neuen Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schalten Sie das Ger\u00e4t nicht ein, wenn Sie Home Assistant starten" + }, + "description": "Konfigurieren Sie allgemeine Ger\u00e4teeinstellungen" } } }, diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json index bbfc9f7b31..945f467c4c 100644 --- a/homeassistant/components/apple_tv/translations/lb.json +++ b/homeassistant/components/apple_tv/translations/lb.json @@ -1,6 +1,52 @@ { "config": { - "flow_title": "Apple TV: {name}" + "abort": { + "already_configured_device": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Du bass um Punkt fir den Apple TV mam Numm \"{name}\" am Home Assistant dob\u00e4izesetzen.\n\n**Fir de Prozess ofzeschl\u00e9issen, muss Du vill\u00e4icht m\u00e9i PIN-Coden aginn.**\n\nNot\u00e9ier w.e.g dass Du d\u00e4in Apple TV mat d\u00ebser Integratioun *net\" ausschalten kanns. N\u00ebmmen de Mediaspiller am Home Assistant schalt aus!", + "title": "Apple TV dob\u00e4isetzen best\u00e4tegen" + }, + "pair_no_pin": { + "description": "Kopplung ass n\u00e9ideg fir de `{protocol}` Service. G\u00ebff de PIN {pin} op dengem Apple TV an fir w\u00e9iderzefueren", + "title": "Kopplung" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Code" + }, + "description": "Kopplung ass n\u00e9ideg fir de `{protocol}` Protokoll. G\u00ebff de PIN code un deen um Ecran ugewise g\u00ebtt. Nullen op der 1ter Plaatz ginn ewechgelooss, dh g\u00ebff 123 wann de gewise Code 0123 ass.", + "title": "Kopplung" + }, + "reconfigure": { + "description": "D\u00ebsen Apple TV huet e puer Verbindungsschwieregkeeten a muss nei konfigur\u00e9iert ginn.", + "title": "Apparat Rekonfiguratioun" + }, + "user": { + "data": { + "device_input": "Apparat" + }, + "description": "F\u00e4nk un andeems Du den Numm vum Apparat (z. B. Kichen oder Schlofkummer) oder IP Adress vum Apple TV deen soll dob\u00e4igesat ginn ag\u00ebss.\n\nFalls d\u00e4in Apparat nez ugewise g\u00ebtt oder iergendwelch Problemer hues, prob\u00e9ier d'IP Adress vum Apparat anzeginn.\n\n{devices}", + "title": "Neien Apple TV ariichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schlalt den Apparat net un wann den Home Assistant start" + }, + "description": "Allgemeng Apparat Astellungen konfigur\u00e9ieren" + } + } }, "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index b69e8de8f7..ed0ef205ff 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "client_id": "Client-ID (optional)", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/hyperion/translations/cs.json b/homeassistant/components/hyperion/translations/cs.json index 46f68baeb1..52e3f0beb5 100644 --- a/homeassistant/components/hyperion/translations/cs.json +++ b/homeassistant/components/hyperion/translations/cs.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/hyperion/translations/de.json b/homeassistant/components/hyperion/translations/de.json new file mode 100644 index 0000000000..0d6bcb6747 --- /dev/null +++ b/homeassistant/components/hyperion/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/et.json b/homeassistant/components/hyperion/translations/et.json index e8d6232236..a225b7f2c4 100644 --- a/homeassistant/components/hyperion/translations/et.json +++ b/homeassistant/components/hyperion/translations/et.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Loodud juurdep\u00e4\u00e4sut\u00f5endiga autentimine nurjus", "auth_required_error": "Autoriseerimise vajalikkuse tuvastamine nurjus", "cannot_connect": "\u00dchendamine nurjus", - "no_id": "Hyperion Ambilighti eksemplar ei teatanud oma ID-d" + "no_id": "Hyperion Ambilighti eksemplar ei teatanud oma ID-d", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index 0510da2305..ff3170ffb9 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Autenticazione utilizzando il token appena creato non riuscita", "auth_required_error": "Impossibile determinare se \u00e8 necessaria l'autorizzazione", "cannot_connect": "Impossibile connettersi", - "no_id": "L'istanza Hyperion Ambilight non ha segnalato il suo ID" + "no_id": "L'istanza Hyperion Ambilight non ha segnalato il suo ID", + "reauth_successful": "Ri-autenticazione completata con successo" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json index 79c90379f1..c73f520597 100644 --- a/homeassistant/components/hyperion/translations/no.json +++ b/homeassistant/components/hyperion/translations/no.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Kunne ikke godkjenne ved hjelp av nylig opprettet token", "auth_required_error": "Kan ikke fastsl\u00e5 om autorisasjon er n\u00f8dvendig", "cannot_connect": "Tilkobling mislyktes", - "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en" + "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en", + "reauth_successful": "Reautentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/hyperion/translations/ru.json b/homeassistant/components/hyperion/translations/ru.json index fda9ef4bb5..9e74680a95 100644 --- a/homeassistant/components/hyperion/translations/ru.json +++ b/homeassistant/components/hyperion/translations/ru.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.", "auth_required_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043b\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "no_id": "Hyperion Ambilight \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0438\u043b \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440." + "no_id": "Hyperion Ambilight \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0438\u043b \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json index fb9cbe3b7a..ed003131bf 100644 --- a/homeassistant/components/hyperion/translations/zh-Hant.json +++ b/homeassistant/components/hyperion/translations/zh-Hant.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u5bc6\u9470\u8a8d\u8b49\u5931\u6557", "auth_required_error": "\u7121\u6cd5\u5224\u5b9a\u662f\u5426\u9700\u8981\u9a57\u8b49", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID" + "no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ipma/translations/de.json b/homeassistant/components/ipma/translations/de.json index 62e2e1e59c..9fa766c190 100644 --- a/homeassistant/components/ipma/translations/de.json +++ b/homeassistant/components/ipma/translations/de.json @@ -15,5 +15,10 @@ "title": "Standort" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA-API-Endpunkt erreichbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index d14dfa6c65..99d11e5d6c 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80", "unknown": "Unerwarteter Fehler" }, "step": { @@ -12,8 +14,11 @@ "data": { "host": "URL", "password": "Passwort", + "tls": "Die TLS-Version des ISY-Controllers.", "username": "Benutzername" - } + }, + "description": "Der Hosteintrag muss im vollst\u00e4ndigen URL-Format vorliegen, z. B. http://192.168.10.100:80", + "title": "Stellen Sie eine Verbindung zu Ihrem ISY994 her" } } }, @@ -21,8 +26,10 @@ "step": { "init": { "data": { - "ignore_string": "Zeichenfolge ignorieren" - } + "ignore_string": "Zeichenfolge ignorieren", + "restore_light_state": "Lichthelligkeit wiederherstellen" + }, + "title": "ISY994 Optionen" } } } diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json new file mode 100644 index 0000000000..3fc69f8594 --- /dev/null +++ b/homeassistant/components/kulersky/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "Wollen Sie mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 12f0a2859e..13f8c6bd80 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { diff --git a/homeassistant/components/mobile_app/translations/de.json b/homeassistant/components/mobile_app/translations/de.json index 0a2f8461be..2289539915 100644 --- a/homeassistant/components/mobile_app/translations/de.json +++ b/homeassistant/components/mobile_app/translations/de.json @@ -8,5 +8,10 @@ "description": "M\u00f6chtest du die Mobile App-Komponente einrichten?" } } + }, + "device_automation": { + "action_type": { + "notify": "Eine Benachrichtigung senden" + } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index bc53bc93f7..5d45710f4a 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "create_entry": { - "default": "Viz [dokumentace Neato]({docs_url})." + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Chcete za\u010d\u00edt nastavovat?" + }, "user": { "data": { "password": "Heslo", @@ -21,5 +31,6 @@ "title": "Informace o \u00fa\u010dtu Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index c41d4e6d93..426dbbe399 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -1,12 +1,27 @@ { "config": { "abort": { - "already_configured": "Bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "create_entry": { - "default": "Siehe [Neato-Dokumentation]({docs_url})." + "default": "Erfolgreich authentifiziert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" }, "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "title": "Wollen Sie mit der Einrichtung beginnen?" + }, "user": { "data": { "password": "Passwort", @@ -17,5 +32,6 @@ "title": "Neato-Kontoinformationen" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json index 72ce208db3..0c0aaa5f17 100644 --- a/homeassistant/components/neato/translations/et.json +++ b/homeassistant/components/neato/translations/et.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "invalid_auth": "Tuvastamise viga" + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "invalid_auth": "Tuvastamise viga", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "create_entry": { - "default": "Vaata [Neato documentation] ( {docs_url} )." + "default": "Tuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamise viga", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "title": "Kas soovid alustada seadistamist?" + }, "user": { "data": { "password": "Salas\u00f5na", @@ -22,5 +32,6 @@ "title": "Neato konto teave" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 989bf9ce13..614465472e 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -2,7 +2,11 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "invalid_auth": "Autenticazione non valida" + "authorize_url_timeout": "Timeout nella generazione dell'URL di autorizzazione.", + "invalid_auth": "Autenticazione non valida", + "missing_configuration": "Questo componente non \u00e8 configurato. Per favore segui la documentazione.", + "no_url_available": "Nessun URL disponibile. Per altre informazioni su questo errore, [controlla la sezione di aiuto]({docs_url})", + "reauth_successful": "Ri-autenticazione completata con successo" }, "create_entry": { "default": "Vedere la [Documentazione di Neato]({docs_url})." @@ -12,6 +16,12 @@ "unknown": "Errore imprevisto" }, "step": { + "pick_implementation": { + "title": "Scegli un metodo di autenticazione" + }, + "reauth_confirm": { + "title": "Vuoi cominciare la configurazione?" + }, "user": { "data": { "password": "Password", @@ -22,5 +32,6 @@ "title": "Informazioni sull'account Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index fcbc0361c2..dbec3effa1 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "invalid_auth": "Ugyldig godkjenning" + "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "invalid_auth": "Ugyldig godkjenning", + "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "reauth_successful": "Reautentisering var vellykket" }, "create_entry": { - "default": "Se [Neato dokumentasjon]({docs_url})." + "default": "Vellykket godkjenning" }, "error": { "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "title": "Vil du starte oppsettet?" + }, "user": { "data": { "password": "Passord", @@ -22,5 +32,6 @@ "title": "Neato kontoinformasjon" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index e4a3053967..30ea15c60c 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "create_entry": { - "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "title": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -22,5 +32,6 @@ "title": "Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index 602b3d47dc..beddee423a 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "create_entry": { - "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "title": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, "user": { "data": { "password": "\u5bc6\u78bc", @@ -22,5 +32,6 @@ "title": "Neato \u5e33\u865f\u8cc7\u8a0a" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 42847d89fd..400fd8eb93 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -29,7 +29,10 @@ }, "device_automation": { "trigger_type": { - "camera_motion": "Bewegung erkannt" + "camera_motion": "Bewegung erkannt", + "camera_person": "Person erkannt", + "camera_sound": "Ger\u00e4usch erkannt", + "doorbell_chime": "T\u00fcrklingel gedr\u00fcckt" } } } \ No newline at end of file diff --git a/homeassistant/components/nws/translations/cs.json b/homeassistant/components/nws/translations/cs.json index f2450bf6f6..2c8402a716 100644 --- a/homeassistant/components/nws/translations/cs.json +++ b/homeassistant/components/nws/translations/cs.json @@ -15,7 +15,7 @@ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", "station": "K\u00f3d stanice METAR" }, - "description": "Pokud nen\u00ed zad\u00e1n k\u00f3d stanice METAR, pou\u017eije se zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka a d\u00e9lka k vyhled\u00e1n\u00ed nejbli\u017e\u0161\u00ed stanice.", + "description": "Pokud nen\u00ed zad\u00e1n k\u00f3d stanice METAR, pou\u017eije se zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka a d\u00e9lka k vyhled\u00e1n\u00ed nejbli\u017e\u0161\u00ed stanice. Prozat\u00edm m\u016f\u017ee b\u00fdt kl\u00ed\u010d API cokoli. Doporu\u010duje se pou\u017e\u00edt platnou e-mailovou adresu.", "title": "P\u0159ipojen\u00ed k National Weather Service" } } diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json index 81a2390cc8..815b87f2ec 100644 --- a/homeassistant/components/ozw/translations/de.json +++ b/homeassistant/components/ozw/translations/de.json @@ -1,7 +1,14 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet" + }, + "step": { + "hassio_confirm": { + "title": "Einrichten der OpenZWave Integration mit dem OpenZWave Add-On" + } } } } \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 8fc660e812..c7dfe6d02b 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -8,7 +8,8 @@ "data": { "email": "E-Mail", "password": "Passwort" - } + }, + "description": "Wollen Sie mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 496c279ba2..e3183fb5c5 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "flow_title": "Tuya Konfiguration", "step": { "user": { @@ -14,7 +17,7 @@ }, "options": { "abort": { - "cannot_connect": "Fehler beim Verbinden" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index a5f4ff11f0..ff3887c840 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -1,10 +1,59 @@ { "config": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "error": { + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" + }, + "flow_title": "\u6d82\u9e26\u914d\u7f6e", "step": { "user": { "data": { + "country_code": "\u60a8\u7684\u5e10\u6237\u56fd\u5bb6(\u5730\u533a)\u4ee3\u7801\uff08\u4f8b\u5982\u4e2d\u56fd\u4e3a 86\uff0c\u7f8e\u56fd\u4e3a 1\uff09", + "password": "\u5bc6\u7801", + "platform": "\u60a8\u6ce8\u518c\u5e10\u6237\u7684\u5e94\u7528", "username": "\u7528\u6237\u540d" - } + }, + "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002", + "title": "\u6d82\u9e26" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "error": { + "dev_multi_type": "\u591a\u4e2a\u8981\u914d\u7f6e\u7684\u8bbe\u5907\u5fc5\u987b\u5177\u6709\u76f8\u540c\u7684\u7c7b\u578b", + "dev_not_config": "\u8bbe\u5907\u7c7b\u578b\u4e0d\u53ef\u914d\u7f6e", + "dev_not_found": "\u672a\u627e\u5230\u8bbe\u5907" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u8bbe\u5907\u4f7f\u7528\u7684\u4eae\u5ea6\u8303\u56f4", + "max_kelvin": "\u6700\u9ad8\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", + "max_temp": "\u6700\u9ad8\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "min_kelvin": "\u6700\u4f4e\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", + "min_temp": "\u6700\u4f4e\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "support_color": "\u5f3a\u5236\u652f\u6301\u8c03\u8272", + "tuya_max_coltemp": "\u8bbe\u5907\u62a5\u544a\u7684\u6700\u9ad8\u8272\u6e29", + "unit_of_measurement": "\u8bbe\u5907\u4f7f\u7528\u7684\u6e29\u5ea6\u5355\u4f4d" + }, + "title": "\u914d\u7f6e\u6d82\u9e26\u8bbe\u5907" + }, + "init": { + "data": { + "discovery_interval": "\u53d1\u73b0\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09", + "list_devices": "\u8bf7\u9009\u62e9\u8981\u914d\u7f6e\u7684\u8bbe\u5907\uff0c\u6216\u7559\u7a7a\u4ee5\u4fdd\u5b58\u914d\u7f6e", + "query_device": "\u8bf7\u9009\u62e9\u4f7f\u7528\u67e5\u8be2\u65b9\u6cd5\u7684\u8bbe\u5907\uff0c\u4ee5\u4fbf\u66f4\u5feb\u5730\u66f4\u65b0\u72b6\u6001", + "query_interval": "\u67e5\u8be2\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09" + }, + "description": "\u8bf7\u4e0d\u8981\u5c06\u8f6e\u8be2\u95f4\u9694\u8bbe\u7f6e\u5f97\u592a\u4f4e\uff0c\u5426\u5219\u5c06\u8c03\u7528\u5931\u8d25\u5e76\u5728\u65e5\u5fd7\u751f\u6210\u9519\u8bef\u6d88\u606f", + "title": "\u914d\u7f6e\u6d82\u9e26\u9009\u9879" } } } diff --git a/homeassistant/components/zerproc/translations/de.json b/homeassistant/components/zerproc/translations/de.json index fdbf897123..dfc337fc84 100644 --- a/homeassistant/components/zerproc/translations/de.json +++ b/homeassistant/components/zerproc/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { From 8c8e6075483ce6fe2f293c3bb54640b4c5f0d2ce Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 17 Dec 2020 23:03:54 -0800 Subject: [PATCH 164/302] Alphabetize hyperion const.py (#44343) --- homeassistant/components/hyperion/const.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 6d61af0b66..2bb9ec241e 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -1,22 +1,22 @@ """Constants for Hyperion integration.""" -DOMAIN = "hyperion" + +CONF_AUTH_ID = "auth_id" +CONF_CREATE_TOKEN = "create_token" +CONF_INSTANCE = "instance" +CONF_ON_UNLOAD = "ON_UNLOAD" +CONF_PRIORITY = "priority" +CONF_ROOT_CLIENT = "ROOT_CLIENT" DEFAULT_NAME = "Hyperion" DEFAULT_ORIGIN = "Home Assistant" DEFAULT_PRIORITY = 128 -CONF_AUTH_ID = "auth_id" -CONF_CREATE_TOKEN = "create_token" -CONF_INSTANCE = "instance" -CONF_PRIORITY = "priority" +DOMAIN = "hyperion" -CONF_ROOT_CLIENT = "ROOT_CLIENT" -CONF_ON_UNLOAD = "ON_UNLOAD" +HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" +HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}" SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}" -HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" -HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" - TYPE_HYPERION_LIGHT = "hyperion_light" From 5c843a1597a375007af8ed95fdf9585cd2f8c7e4 Mon Sep 17 00:00:00 2001 From: rubenbe Date: Fri, 18 Dec 2020 08:37:32 +0100 Subject: [PATCH 165/302] Update pytradfri to 7.0.5 (#44347) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 7ffa8ed24b..57f58f0599 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.4"], + "requirements": ["pytradfri[async]==7.0.5"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 3a04884562..3d61022860 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1858,7 +1858,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.4 +pytradfri[async]==7.0.5 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 992b3d0816..7ffaf2f83c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -915,7 +915,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.4 +pytradfri[async]==7.0.5 # homeassistant.components.vera pyvera==0.3.11 From 5b14c1da7314a6b84cb1e5475169967d99902c46 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Fri, 18 Dec 2020 00:46:59 -0700 Subject: [PATCH 166/302] Bump pyps4-2ndscreen to 1.2.0 (#44273) * Bump PS4 to 1.2.0 * Add additional media_player tests * Fix tests --- homeassistant/components/ps4/config_flow.py | 11 ++++++++-- homeassistant/components/ps4/manifest.json | 2 +- homeassistant/components/ps4/media_player.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ps4/test_config_flow.py | 3 ++- tests/components/ps4/test_media_player.py | 22 ++++++++++++++++++-- 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index a8bdb2b9c4..a494b96abf 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -23,6 +23,7 @@ CONF_MODE = "Config Mode" CONF_AUTO = "Auto Discover" CONF_MANUAL = "Manual Entry" +LOCAL_UDP_PORT = 1988 UDP_PORT = 987 TCP_PORT = 997 PORT_MSG = {UDP_PORT: "port_987_bind_error", TCP_PORT: "port_997_bind_error"} @@ -107,8 +108,9 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): if user_input is None: # Search for device. + # If LOCAL_UDP_PORT cannot be used, a random port will be selected. devices = await self.hass.async_add_executor_job( - self.helper.has_devices, self.m_device + self.helper.has_devices, self.m_device, LOCAL_UDP_PORT ) # Abort if can't find device. @@ -147,7 +149,12 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): self.host = user_input[CONF_IP_ADDRESS] is_ready, is_login = await self.hass.async_add_executor_job( - self.helper.link, self.host, self.creds, self.pin, DEFAULT_ALIAS + self.helper.link, + self.host, + self.creds, + self.pin, + DEFAULT_ALIAS, + LOCAL_UDP_PORT, ) if is_ready is False: diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 3527a05e5b..500c243b8c 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,6 +3,6 @@ "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": ["pyps4-2ndscreen==1.1.1"], + "requirements": ["pyps4-2ndscreen==1.2.0"], "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 8ef9413edb..24a1589db0 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -3,6 +3,7 @@ import asyncio import logging from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete +from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP import pyps4_2ndscreen.ps4 as pyps4 from homeassistant.components.media_player import MediaPlayerEntity @@ -262,7 +263,7 @@ class PS4Device(MediaPlayerEntity): app_name = title.name art = title.cover_art # Assume media type is game if not app. - if title.game_type != "App": + if title.game_type != PS_TYPE_APP: media_type = MEDIA_TYPE_GAME else: media_type = MEDIA_TYPE_APP diff --git a/requirements_all.txt b/requirements_all.txt index 3d61022860..fadf7ea3f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1622,7 +1622,7 @@ pypoint==2.0.0 pyprof2calltree==1.4.5 # homeassistant.components.ps4 -pyps4-2ndscreen==1.1.1 +pyps4-2ndscreen==1.2.0 # homeassistant.components.qvr_pro pyqvrpro==0.52 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ffaf2f83c..755a5d6fac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -823,7 +823,7 @@ pypoint==2.0.0 pyprof2calltree==1.4.5 # homeassistant.components.ps4 -pyps4-2ndscreen==1.1.1 +pyps4-2ndscreen==1.2.0 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 0febf142d0..8ef1133519 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -4,6 +4,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components import ps4 +from homeassistant.components.ps4.config_flow import LOCAL_UDP_PORT from homeassistant.components.ps4.const import ( DEFAULT_ALIAS, DEFAULT_NAME, @@ -360,7 +361,7 @@ async def test_0_pin(hass): result["flow_id"], mock_config ) mock_call.assert_called_once_with( - MOCK_HOST, MOCK_CREDS, MOCK_CODE_LEAD_0_STR, DEFAULT_ALIAS + MOCK_HOST, MOCK_CREDS, MOCK_CODE_LEAD_0_STR, DEFAULT_ALIAS, LOCAL_UDP_PORT ) diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 4e22591d3b..8a03f13bed 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,5 +1,7 @@ """Tests for the PS4 media player platform.""" from pyps4_2ndscreen.credential import get_ddp_message +from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT +from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -8,6 +10,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, + MEDIA_TYPE_APP, MEDIA_TYPE_GAME, ) from homeassistant.components.ps4.const import ( @@ -149,7 +152,7 @@ async def setup_mock_component(hass, entry=None): async def mock_ddp_response(hass, mock_status_data): """Mock raw UDP response from device.""" mock_protocol = hass.data[PS4_DATA].protocol - + assert mock_protocol.local_port == DEFAULT_UDP_PORT mock_code = mock_status_data.get("status_code") mock_status = mock_status_data.get("status") mock_status_header = f"{mock_code} {mock_status}" @@ -224,7 +227,7 @@ async def test_media_attributes_are_fetched(hass): mock_result = MagicMock() mock_result.name = MOCK_TITLE_NAME mock_result.cover_art = MOCK_TITLE_ART_URL - mock_result.game_type = "game" + mock_result.game_type = "not_an_app" with patch(mock_func, return_value=mock_result) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) @@ -241,6 +244,21 @@ async def test_media_attributes_are_fetched(hass): assert mock_attrs.get(ATTR_MEDIA_TITLE) == MOCK_TITLE_NAME assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE + # Change state so that the next fetch is called. + await mock_ddp_response(hass, MOCK_STATUS_STANDBY) + + # Test that content type of app is set. + mock_result.game_type = PS_TYPE_APP + + with patch(mock_func, return_value=mock_result) as mock_fetch_app: + await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + + mock_state = hass.states.get(mock_entity_id) + mock_attrs = dict(mock_state.attributes) + + assert len(mock_fetch_app.mock_calls) == 1 + assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + async def test_media_attributes_are_loaded(hass, patch_load_json): """Test that media attributes are loaded.""" From 8adfb5dff4283fa69516ba2686931917bfb80610 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Dec 2020 10:47:18 +0100 Subject: [PATCH 167/302] Bump codecov/codecov-action from v1.0.15 to v1.1.0 (#44346) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.0.15 to v1.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.0.15...7de43a7373de21874ae196a78f8eb633fcf7f0c4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56b181aa02..27bf0414dd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -785,4 +785,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.0.15 + uses: codecov/codecov-action@v1.1.0 From ad634a393b352b8f8b3000debad386cef0cce5b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Dec 2020 10:53:49 +0100 Subject: [PATCH 168/302] Bump actions/setup-python from v2.1.4 to v2.2.0 (#44345) Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.1.4 to v2.2.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.1.4...8c5ea631b2b2d5d8840cf4a2b183a8a0edc1e40d) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 27bf0414dd..cd394234cc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -73,7 +73,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -118,7 +118,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -163,7 +163,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -230,7 +230,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -278,7 +278,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -326,7 +326,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -371,7 +371,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -419,7 +419,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -475,7 +475,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -555,7 +555,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} From ae596c7dff4536b7d26a899a89ae88c0588e819a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Dec 2020 12:28:18 -0700 Subject: [PATCH 169/302] Fix bug in unloading RainMachine options listener (#44359) * Fix bug in unloading RainMachine options listener * Order --- homeassistant/components/rainmachine/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 41c56e38db..c8697adbcd 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -275,7 +275,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ]: hass.services.async_register(DOMAIN, service, method, schema=schema) - hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) + hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( + async_reload_entry + ) return True From ea578f57679a2b82cdcec43dd73e7babc4e52e60 Mon Sep 17 00:00:00 2001 From: Heisenberg <58039006+elbueno222@users.noreply.github.com> Date: Fri, 18 Dec 2020 19:29:16 +0000 Subject: [PATCH 170/302] Update sensor.py (#44350) BME280 sensor has a resolution of 0.01 degree (20 bits) so temperature values should be rounded to 2 decimal places --- homeassistant/components/bme280/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 5f38c420ff..265ec01b6d 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -169,9 +169,9 @@ class BME280Sensor(Entity): await self.hass.async_add_executor_job(self.bme280_client.update) if self.bme280_client.sensor.sample_ok: if self.type == SENSOR_TEMP: - temperature = round(self.bme280_client.sensor.temperature, 1) + temperature = round(self.bme280_client.sensor.temperature, 2) if self.temp_unit == TEMP_FAHRENHEIT: - temperature = round(celsius_to_fahrenheit(temperature), 1) + temperature = round(celsius_to_fahrenheit(temperature), 2) self._state = temperature elif self.type == SENSOR_HUMID: self._state = round(self.bme280_client.sensor.humidity, 1) From 14432248b0cb0637a4168eac03cbfb497cd10491 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Dec 2020 13:12:16 -0700 Subject: [PATCH 171/302] Bump pyiqvia to 0.3.1 (#44358) --- homeassistant/components/iqvia/__init__.py | 2 +- homeassistant/components/iqvia/config_flow.py | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index db0df3a073..bf1725a036 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -52,7 +52,7 @@ async def async_setup_entry(hass, entry): ) websession = aiohttp_client.async_get_clientsession(hass) - client = Client(entry.data[CONF_ZIP_CODE], websession) + client = Client(entry.data[CONF_ZIP_CODE], session=websession) async def async_get_data_from_api(api_coro): """Get data from a particular API coroutine.""" diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index e43c61985d..ecd1e3c3c4 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -30,7 +30,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): websession = aiohttp_client.async_get_clientsession(self.hass) try: - Client(user_input[CONF_ZIP_CODE], websession) + Client(user_input[CONF_ZIP_CODE], session=websession) except InvalidZipError: return self.async_show_form( step_id="user", diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 5ab331df44..6445b4ad91 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.2", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index fadf7ea3f2..411b782577 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.2.1 +pyiqvia==0.3.1 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 755a5d6fac..e22f5503fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -734,7 +734,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.2.1 +pyiqvia==0.3.1 # homeassistant.components.isy994 pyisy==2.1.0 From 0fddaedc1254efeb96826b95033519d5d293c13e Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Fri, 18 Dec 2020 15:15:08 -0500 Subject: [PATCH 172/302] Adjust Rachio logging level when adding shared controllers (#44323) * change logging level * unnecessary continue * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/rachio/device.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 2ef904f868..c9de7eea7d 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_DEVICES = "devices" ATTR_DURATION = "duration" +PERMISSION_ERROR = "7" PAUSE_SERVICE_SCHEMA = vol.Schema( { @@ -78,11 +79,18 @@ class RachioPerson: # webhooks are normally a list, however if there is an error # rachio hands us back a dict if isinstance(webhooks, dict): - _LOGGER.error( - "Failed to add rachio controller '%s' because of an error: %s", - controller[KEY_NAME], - webhooks.get("error", "Unknown Error"), - ) + if webhooks.get("code") == PERMISSION_ERROR: + _LOGGER.info( + "Not adding controller '%s', only controllers owned by '%s' may be added", + controller[KEY_NAME], + self.username, + ) + else: + _LOGGER.error( + "Failed to add rachio controller '%s' because of an error: %s", + controller[KEY_NAME], + webhooks.get("error", "Unknown Error"), + ) continue rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) From 61c57d4d563a2c55bc42e0631098ded0e3857ab8 Mon Sep 17 00:00:00 2001 From: emufan Date: Fri, 18 Dec 2020 21:27:12 +0100 Subject: [PATCH 173/302] Add another xml content type for JSON conversion in RESTful sensor (#44312) * Update sensor.py Some xml-result is passed via "application/xhtml+xml" instead of text/xml or application/xml. And then the xml structure is not parsed to json as expected. I changed it locally, but suggest to add thise here as well. * Update homeassistant/components/rest/sensor.py Fine for me as well, as it is working this way as well. But I would not see any disadvantage to convert everything, what is possible - a better scrape sensor this way. ;) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- homeassistant/components/rest/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 51d9ec2047..85d79b6b33 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -227,6 +227,7 @@ class RestSensor(Entity): if content_type and ( content_type.startswith("text/xml") or content_type.startswith("application/xml") + or content_type.startswith("application/xhtml+xml") ): try: value = json.dumps(xmltodict.parse(value)) From 1ac937c7d51847011cfdea322ca4747391b6dfa7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 19 Dec 2020 00:03:56 +0000 Subject: [PATCH 174/302] [ci skip] Translation update --- .../components/accuweather/translations/de.json | 9 +++++++++ .../accuweather/translations/sensor.de.json | 9 +++++++++ .../components/hyperion/translations/ca.json | 3 ++- .../components/neato/translations/ca.json | 17 ++++++++++++++--- .../components/neato/translations/it.json | 2 +- 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.de.json diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 1dd547b6fc..9c924e7d97 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -11,6 +11,15 @@ } } }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Wettervorhersage" + } + } + } + }, "system_health": { "info": { "remaining_requests": "Verbleibende erlaubte Anfragen" diff --git a/homeassistant/components/accuweather/translations/sensor.de.json b/homeassistant/components/accuweather/translations/sensor.de.json new file mode 100644 index 0000000000..7ccc7c7360 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Fallend", + "rising": "Steigend", + "steady": "Gleichbleibend" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ca.json b/homeassistant/components/hyperion/translations/ca.json index 20222bd0bc..50ac384d8c 100644 --- a/homeassistant/components/hyperion/translations/ca.json +++ b/homeassistant/components/hyperion/translations/ca.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "No s'ha pogut autenticar amb el nou token creat", "auth_required_error": "No s'ha pogut determinar si cal autoritzaci\u00f3", "cannot_connect": "Ha fallat la connexi\u00f3", - "no_id": "La inst\u00e0ncia d'Hyperion Ambilight no ha retornat el seu ID" + "no_id": "La inst\u00e0ncia d'Hyperion Ambilight no ha retornat el seu ID", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/neato/translations/ca.json b/homeassistant/components/neato/translations/ca.json index 601af3a68e..f818135c51 100644 --- a/homeassistant/components/neato/translations/ca.json +++ b/homeassistant/components/neato/translations/ca.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "create_entry": { - "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + "default": "Autenticaci\u00f3 exitosa" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "title": "Vols comen\u00e7ar la configuraci\u00f3?" + }, "user": { "data": { "password": "Contrasenya", @@ -22,5 +32,6 @@ "title": "Informaci\u00f3 del compte Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 614465472e..100237c33e 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -9,7 +9,7 @@ "reauth_successful": "Ri-autenticazione completata con successo" }, "create_entry": { - "default": "Vedere la [Documentazione di Neato]({docs_url})." + "default": "Autenticato con successo" }, "error": { "invalid_auth": "Autenticazione non valida", From de04a1ed6791dbcb22105bda1d07dc7478dbe05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Dec 2020 13:35:13 +0200 Subject: [PATCH 175/302] Enable more Bandit tests (#44307) https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing --- homeassistant/components/recorder/util.py | 2 +- tests/bandit.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index ed7f5affc5..abf1426868 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -149,7 +149,7 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: - cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # sec: not injection + cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection return True diff --git a/tests/bandit.yaml b/tests/bandit.yaml index ebd284eaa0..568f77d622 100644 --- a/tests/bandit.yaml +++ b/tests/bandit.yaml @@ -1,6 +1,7 @@ # https://bandit.readthedocs.io/en/latest/config.html tests: + - B103 - B108 - B306 - B307 @@ -13,5 +14,8 @@ tests: - B319 - B320 - B325 + - B601 - B602 - B604 + - B608 + - B609 From 317ed418dd19dc7dcf2906ddaf3ad6b04583bf2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Dec 2020 13:46:27 +0200 Subject: [PATCH 176/302] Use singleton enum for "not set" sentinels (#41990) * Use singleton enum for "not set" sentinel https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions * Remove unused variable --- homeassistant/components/camera/prefs.py | 7 +- homeassistant/components/cloud/prefs.py | 42 ++++----- homeassistant/components/deconz/__init__.py | 4 +- homeassistant/components/person/__init__.py | 2 - homeassistant/config_entries.py | 23 +++-- homeassistant/core.py | 2 +- homeassistant/helpers/device_registry.py | 97 ++++++++++----------- homeassistant/helpers/entity_registry.py | 67 +++++++------- homeassistant/helpers/typing.py | 10 +++ homeassistant/loader.py | 2 +- homeassistant/requirements.py | 14 +-- 11 files changed, 139 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index ae182c62dc..ec35a44840 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,11 +1,12 @@ """Preference management for camera component.""" +from homeassistant.helpers.typing import UNDEFINED + from .const import DOMAIN, PREF_PRELOAD_STREAM # mypy: allow-untyped-defs, no-check-untyped-defs STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -_UNDEF = object() class CameraEntityPreferences: @@ -44,14 +45,14 @@ class CameraPreferences: self._prefs = prefs async def async_update( - self, entity_id, *, preload_stream=_UNDEF, stream_options=_UNDEF + self, entity_id, *, preload_stream=UNDEFINED, stream_options=UNDEFINED ): """Update camera preferences.""" if not self._prefs.get(entity_id): self._prefs[entity_id] = {} for key, value in ((PREF_PRELOAD_STREAM, preload_stream),): - if value is not _UNDEF: + if value is not UNDEFINED: self._prefs[entity_id][key] = value await self._store.async_save(self._prefs) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0a41f8e2a8..6e0e78839c 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -5,6 +5,7 @@ from typing import List, Optional from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User from homeassistant.core import callback +from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.logging import async_create_catching_coro from .const import ( @@ -36,7 +37,6 @@ from .const import ( STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -_UNDEF = object() class CloudPreferences: @@ -74,18 +74,18 @@ class CloudPreferences: async def async_update( self, *, - google_enabled=_UNDEF, - alexa_enabled=_UNDEF, - remote_enabled=_UNDEF, - google_secure_devices_pin=_UNDEF, - cloudhooks=_UNDEF, - cloud_user=_UNDEF, - google_entity_configs=_UNDEF, - alexa_entity_configs=_UNDEF, - alexa_report_state=_UNDEF, - google_report_state=_UNDEF, - alexa_default_expose=_UNDEF, - google_default_expose=_UNDEF, + google_enabled=UNDEFINED, + alexa_enabled=UNDEFINED, + remote_enabled=UNDEFINED, + google_secure_devices_pin=UNDEFINED, + cloudhooks=UNDEFINED, + cloud_user=UNDEFINED, + google_entity_configs=UNDEFINED, + alexa_entity_configs=UNDEFINED, + alexa_report_state=UNDEFINED, + google_report_state=UNDEFINED, + alexa_default_expose=UNDEFINED, + google_default_expose=UNDEFINED, ): """Update user preferences.""" prefs = {**self._prefs} @@ -104,7 +104,7 @@ class CloudPreferences: (PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose), (PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose), ): - if value is not _UNDEF: + if value is not UNDEFINED: prefs[key] = value if remote_enabled is True and self._has_local_trusted_network: @@ -121,10 +121,10 @@ class CloudPreferences: self, *, entity_id, - override_name=_UNDEF, - disable_2fa=_UNDEF, - aliases=_UNDEF, - should_expose=_UNDEF, + override_name=UNDEFINED, + disable_2fa=UNDEFINED, + aliases=UNDEFINED, + should_expose=UNDEFINED, ): """Update config for a Google entity.""" entities = self.google_entity_configs @@ -137,7 +137,7 @@ class CloudPreferences: (PREF_ALIASES, aliases), (PREF_SHOULD_EXPOSE, should_expose), ): - if value is not _UNDEF: + if value is not UNDEFINED: changes[key] = value if not changes: @@ -149,7 +149,7 @@ class CloudPreferences: await self.async_update(google_entity_configs=updated_entities) async def async_update_alexa_entity_config( - self, *, entity_id, should_expose=_UNDEF + self, *, entity_id, should_expose=UNDEFINED ): """Update config for an Alexa entity.""" entities = self.alexa_entity_configs @@ -157,7 +157,7 @@ class CloudPreferences: changes = {} for key, value in ((PREF_SHOULD_EXPOSE, should_expose),): - if value is not _UNDEF: + if value is not UNDEFINED: changes[key] = value if not changes: diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 507b48da9d..fec7b82e36 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,8 +1,8 @@ """Support for deCONZ devices.""" import voluptuous as vol -from homeassistant.config_entries import _UNDEF from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.typing import UNDEFINED from .config_flow import get_master_gateway from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry): # 0.104 introduced config entry unique id, this makes upgrading possible if config_entry.unique_id is None: - new_data = _UNDEF + new_data = UNDEFINED if CONF_BRIDGE_ID in config_entry.data: new_data = dict(config_entry.data) new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID] diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 584ce708d1..d0c0e9eccc 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -87,8 +87,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_UNDEF = object() - @bind_hass async def async_create_person(hass, name, *, user_id=None, device_trackers=None): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c42a53b2da..601ce1efbf 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -13,12 +13,12 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.event import Event +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util _LOGGER = logging.getLogger(__name__) -_UNDEF: dict = {} SOURCE_DISCOVERY = "discovery" SOURCE_HASSIO = "hassio" @@ -760,12 +760,11 @@ class ConfigEntries: self, entry: ConfigEntry, *, - # pylint: disable=dangerous-default-value # _UNDEFs not modified - unique_id: Union[str, dict, None] = _UNDEF, - title: Union[str, dict] = _UNDEF, - data: dict = _UNDEF, - options: dict = _UNDEF, - system_options: dict = _UNDEF, + unique_id: Union[str, dict, None, UndefinedType] = UNDEFINED, + title: Union[str, dict, UndefinedType] = UNDEFINED, + data: Union[dict, UndefinedType] = UNDEFINED, + options: Union[dict, UndefinedType] = UNDEFINED, + system_options: Union[dict, UndefinedType] = UNDEFINED, ) -> bool: """Update a config entry. @@ -777,24 +776,24 @@ class ConfigEntries: """ changed = False - if unique_id is not _UNDEF and entry.unique_id != unique_id: + if unique_id is not UNDEFINED and entry.unique_id != unique_id: changed = True entry.unique_id = cast(Optional[str], unique_id) - if title is not _UNDEF and entry.title != title: + if title is not UNDEFINED and entry.title != title: changed = True entry.title = cast(str, title) - if data is not _UNDEF and entry.data != data: # type: ignore + if data is not UNDEFINED and entry.data != data: # type: ignore changed = True entry.data = MappingProxyType(data) - if options is not _UNDEF and entry.options != options: # type: ignore + if options is not UNDEFINED and entry.options != options: # type: ignore changed = True entry.options = MappingProxyType(options) if ( - system_options is not _UNDEF + system_options is not UNDEFINED and entry.system_options.as_dict() != system_options ): changed = True diff --git a/homeassistant/core.py b/homeassistant/core.py index 9eeaf6fccc..01c4047af6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -89,7 +89,7 @@ block_async_io.enable() fix_threading_exception_logging() T = TypeVar("T") -_UNDEF: dict = {} +_UNDEF: dict = {} # Internal; not helpers.typing.UNDEFINED due to circular dependency # pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) CALLBACK_TYPE = Callable[[], None] diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cc8f9a1782..6e8c09bbd6 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,7 +11,7 @@ import homeassistant.util.uuid as uuid_util from .debounce import Debouncer from .singleton import singleton -from .typing import HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType if TYPE_CHECKING: from . import entity_registry @@ -19,7 +19,6 @@ if TYPE_CHECKING: # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) -_UNDEF = object() DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" @@ -224,17 +223,17 @@ class DeviceRegistry: config_entry_id, connections=None, identifiers=None, - manufacturer=_UNDEF, - model=_UNDEF, - name=_UNDEF, - default_manufacturer=_UNDEF, - default_model=_UNDEF, - default_name=_UNDEF, - sw_version=_UNDEF, - entry_type=_UNDEF, + manufacturer=UNDEFINED, + model=UNDEFINED, + name=UNDEFINED, + default_manufacturer=UNDEFINED, + default_model=UNDEFINED, + default_name=UNDEFINED, + sw_version=UNDEFINED, + entry_type=UNDEFINED, via_device=None, # To disable a device if it gets created - disabled_by=_UNDEF, + disabled_by=UNDEFINED, ): """Get device. Create if it doesn't exist.""" if not identifiers and not connections: @@ -261,27 +260,27 @@ class DeviceRegistry: ) self._add_device(device) - if default_manufacturer is not _UNDEF and device.manufacturer is None: + if default_manufacturer is not UNDEFINED and device.manufacturer is None: manufacturer = default_manufacturer - if default_model is not _UNDEF and device.model is None: + if default_model is not UNDEFINED and device.model is None: model = default_model - if default_name is not _UNDEF and device.name is None: + if default_name is not UNDEFINED and device.name is None: name = default_name if via_device is not None: via = self.async_get_device({via_device}, set()) - via_device_id = via.id if via else _UNDEF + via_device_id = via.id if via else UNDEFINED else: - via_device_id = _UNDEF + via_device_id = UNDEFINED return self._async_update_device( device.id, add_config_entry_id=config_entry_id, via_device_id=via_device_id, - merge_connections=connections or _UNDEF, - merge_identifiers=identifiers or _UNDEF, + merge_connections=connections or UNDEFINED, + merge_identifiers=identifiers or UNDEFINED, manufacturer=manufacturer, model=model, name=name, @@ -295,16 +294,16 @@ class DeviceRegistry: self, device_id, *, - area_id=_UNDEF, - manufacturer=_UNDEF, - model=_UNDEF, - name=_UNDEF, - name_by_user=_UNDEF, - new_identifiers=_UNDEF, - sw_version=_UNDEF, - via_device_id=_UNDEF, - remove_config_entry_id=_UNDEF, - disabled_by=_UNDEF, + area_id=UNDEFINED, + manufacturer=UNDEFINED, + model=UNDEFINED, + name=UNDEFINED, + name_by_user=UNDEFINED, + new_identifiers=UNDEFINED, + sw_version=UNDEFINED, + via_device_id=UNDEFINED, + remove_config_entry_id=UNDEFINED, + disabled_by=UNDEFINED, ): """Update properties of a device.""" return self._async_update_device( @@ -326,20 +325,20 @@ class DeviceRegistry: self, device_id, *, - add_config_entry_id=_UNDEF, - remove_config_entry_id=_UNDEF, - merge_connections=_UNDEF, - merge_identifiers=_UNDEF, - new_identifiers=_UNDEF, - manufacturer=_UNDEF, - model=_UNDEF, - name=_UNDEF, - sw_version=_UNDEF, - entry_type=_UNDEF, - via_device_id=_UNDEF, - area_id=_UNDEF, - name_by_user=_UNDEF, - disabled_by=_UNDEF, + add_config_entry_id=UNDEFINED, + remove_config_entry_id=UNDEFINED, + merge_connections=UNDEFINED, + merge_identifiers=UNDEFINED, + new_identifiers=UNDEFINED, + manufacturer=UNDEFINED, + model=UNDEFINED, + name=UNDEFINED, + sw_version=UNDEFINED, + entry_type=UNDEFINED, + via_device_id=UNDEFINED, + area_id=UNDEFINED, + name_by_user=UNDEFINED, + disabled_by=UNDEFINED, ): """Update device attributes.""" old = self.devices[device_id] @@ -349,13 +348,13 @@ class DeviceRegistry: config_entries = old.config_entries if ( - add_config_entry_id is not _UNDEF + add_config_entry_id is not UNDEFINED and add_config_entry_id not in old.config_entries ): config_entries = old.config_entries | {add_config_entry_id} if ( - remove_config_entry_id is not _UNDEF + remove_config_entry_id is not UNDEFINED and remove_config_entry_id in config_entries ): if config_entries == {remove_config_entry_id}: @@ -373,10 +372,10 @@ class DeviceRegistry: ): old_value = getattr(old, attr_name) # If not undefined, check if `value` contains new items. - if value is not _UNDEF and not value.issubset(old_value): + if value is not UNDEFINED and not value.issubset(old_value): changes[attr_name] = old_value | value - if new_identifiers is not _UNDEF: + if new_identifiers is not UNDEFINED: changes["identifiers"] = new_identifiers for attr_name, value in ( @@ -388,13 +387,13 @@ class DeviceRegistry: ("via_device_id", via_device_id), ("disabled_by", disabled_by), ): - if value is not _UNDEF and value != getattr(old, attr_name): + if value is not UNDEFINED and value != getattr(old, attr_name): changes[attr_name] = value - if area_id is not _UNDEF and area_id != old.area_id: + if area_id is not UNDEFINED and area_id != old.area_id: changes["area_id"] = area_id - if name_by_user is not _UNDEF and name_by_user != old.name_by_user: + if name_by_user is not UNDEFINED and name_by_user != old.name_by_user: changes["name_by_user"] = name_by_user if old.is_new: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4582fc5f3b..44f5c9c56f 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -39,7 +39,7 @@ from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml from .singleton import singleton -from .typing import HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry # noqa: F401 @@ -51,7 +51,6 @@ DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) -_UNDEF = object() DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_DEVICE = "device" DISABLED_HASS = "hass" @@ -225,15 +224,15 @@ class EntityRegistry: if entity_id: return self._async_update_entity( # type: ignore entity_id, - config_entry_id=config_entry_id or _UNDEF, - device_id=device_id or _UNDEF, - area_id=area_id or _UNDEF, - capabilities=capabilities or _UNDEF, - 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, + config_entry_id=config_entry_id or UNDEFINED, + device_id=device_id or UNDEFINED, + area_id=area_id or UNDEFINED, + capabilities=capabilities or UNDEFINED, + supported_features=supported_features or UNDEFINED, + device_class=device_class or UNDEFINED, + unit_of_measurement=unit_of_measurement or UNDEFINED, + original_name=original_name or UNDEFINED, + original_icon=original_icon or UNDEFINED, # 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 @@ -333,12 +332,12 @@ class EntityRegistry: self, entity_id, *, - name=_UNDEF, - icon=_UNDEF, - area_id=_UNDEF, - new_entity_id=_UNDEF, - new_unique_id=_UNDEF, - disabled_by=_UNDEF, + name=UNDEFINED, + icon=UNDEFINED, + area_id=UNDEFINED, + new_entity_id=UNDEFINED, + new_unique_id=UNDEFINED, + disabled_by=UNDEFINED, ): """Update properties of an entity.""" return cast( # cast until we have _async_update_entity type hinted @@ -359,20 +358,20 @@ class EntityRegistry: self, entity_id, *, - name=_UNDEF, - icon=_UNDEF, - config_entry_id=_UNDEF, - new_entity_id=_UNDEF, - device_id=_UNDEF, - area_id=_UNDEF, - new_unique_id=_UNDEF, - disabled_by=_UNDEF, - capabilities=_UNDEF, - supported_features=_UNDEF, - device_class=_UNDEF, - unit_of_measurement=_UNDEF, - original_name=_UNDEF, - original_icon=_UNDEF, + name=UNDEFINED, + icon=UNDEFINED, + config_entry_id=UNDEFINED, + new_entity_id=UNDEFINED, + device_id=UNDEFINED, + area_id=UNDEFINED, + new_unique_id=UNDEFINED, + disabled_by=UNDEFINED, + capabilities=UNDEFINED, + supported_features=UNDEFINED, + device_class=UNDEFINED, + unit_of_measurement=UNDEFINED, + original_name=UNDEFINED, + original_icon=UNDEFINED, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -393,10 +392,10 @@ class EntityRegistry: ("original_name", original_name), ("original_icon", original_icon), ): - if value is not _UNDEF and value != getattr(old, attr_name): + if value is not UNDEFINED and value != getattr(old, attr_name): changes[attr_name] = value - if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: + if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): raise ValueError("Entity is already registered") @@ -409,7 +408,7 @@ class EntityRegistry: self.entities.pop(entity_id) entity_id = changes["entity_id"] = new_entity_id - if new_unique_id is not _UNDEF: + if new_unique_id is not UNDEFINED: conflict_entity_id = self.async_get_entity_id( old.domain, old.platform, new_unique_id ) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index bed0d2b8d1..279bc0f686 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,4 +1,5 @@ """Typing Helpers for Home Assistant.""" +from enum import Enum from typing import Any, Dict, Mapping, Optional, Tuple, Union import homeassistant.core @@ -16,3 +17,12 @@ TemplateVarsType = Optional[Mapping[str, Any]] # Custom type for recorder Queries QueryType = Any + + +class UndefinedType(Enum): + """Singleton type for use with not set sentinel values.""" + + _singleton = 0 + + +UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 6dabfdf044..ba29ff4a8d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -48,7 +48,7 @@ CUSTOM_WARNING = ( "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant." ) -_UNDEF = object() +_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency MAX_LOAD_CONCURRENTLY = 4 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index b3af06ad07..cebfd95591 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration import homeassistant.util.package as pkg_util @@ -17,7 +18,6 @@ DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { "ssdp": ("ssdp",), "zeroconf": ("zeroconf", "homekit"), } -_UNDEF = object() class RequirementsNotFound(HomeAssistantError): @@ -53,19 +53,21 @@ async def async_get_integration_with_requirements( if cache is None: cache = hass.data[DATA_INTEGRATIONS_WITH_REQS] = {} - int_or_evt: Union[Integration, asyncio.Event, None] = cache.get(domain, _UNDEF) + int_or_evt: Union[Integration, asyncio.Event, None, UndefinedType] = cache.get( + domain, UNDEFINED + ) if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() - int_or_evt = cache.get(domain, _UNDEF) + int_or_evt = cache.get(domain, UNDEFINED) - # When we have waited and it's _UNDEF, it doesn't exist + # When we have waited and it's UNDEFINED, it doesn't exist # We don't cache that it doesn't exist, or else people can't fix it # and then restart, because their config will never be valid. - if int_or_evt is _UNDEF: + if int_or_evt is UNDEFINED: raise IntegrationNotFound(domain) - if int_or_evt is not _UNDEF: + if int_or_evt is not UNDEFINED: return cast(Integration, int_or_evt) event = cache[domain] = asyncio.Event() From 896f51fd825e1a64e9f2face71e015051ad30178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sat, 19 Dec 2020 15:10:02 +0000 Subject: [PATCH 177/302] Add Wind to Accuweather sensors (#44364) --- homeassistant/components/accuweather/const.py | 21 +++++++ .../components/accuweather/sensor.py | 6 +- tests/components/accuweather/test_sensor.py | 56 +++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index fa9ed6b467..cbccc3a462 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -183,6 +183,20 @@ FORECAST_SENSOR_TYPES = { ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, }, + "WindDay": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Day", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, + "WindNight": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Night", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, } OPTIONAL_SENSORS = ( @@ -284,6 +298,13 @@ SENSOR_TYPES = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, }, + "Wind": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, "WindGust": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-windy", diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 4f61322b2c..90058e254d 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -96,7 +96,7 @@ class AccuWeatherSensor(CoordinatorEntity): return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ self.kind ]["Value"] - if self.kind in ["WindGustDay", "WindGustNight"]: + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ self.kind ]["Speed"]["Value"] @@ -115,7 +115,7 @@ class AccuWeatherSensor(CoordinatorEntity): return self.coordinator.data["PrecipitationSummary"][self.kind][ self._unit_system ]["Value"] - if self.kind == "WindGust": + if self.kind in ["Wind", "WindGust"]: return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"] return self.coordinator.data[self.kind] @@ -144,7 +144,7 @@ class AccuWeatherSensor(CoordinatorEntity): def device_state_attributes(self): """Return the state attributes.""" if self.forecast_day is not None: - if self.kind in ["WindGustDay", "WindGustNight"]: + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ self.forecast_day ][self.kind]["Direction"]["English"] diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index d4aae9a94f..185f602488 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -222,6 +222,13 @@ async def test_sensor_enabled_without_forecast(hass): suggested_object_id="home_wet_bulb_temperature", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-wind", + suggested_object_id="home_wind", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -313,6 +320,20 @@ async def test_sensor_enabled_without_forecast(hass): suggested_object_id="home_wind_gust_night_0d", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windday-0", + suggested_object_id="home_wind_day_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windnight-0", + suggested_object_id="home_wind_night_0d", + disabled_by=None, + ) await init_integration(hass, forecast=True) @@ -393,6 +414,17 @@ async def test_sensor_enabled_without_forecast(hass): assert entry assert entry.unique_id == "0123456-windgust" + state = hass.states.get("sensor.home_wind") + assert state + assert state.state == "14.5" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind") + assert entry + assert entry.unique_id == "0123456-wind" + state = hass.states.get("sensor.home_cloud_cover_day_0d") assert state assert state.state == "58" @@ -507,6 +539,30 @@ async def test_sensor_enabled_without_forecast(hass): assert entry assert entry.unique_id == "0123456-tree-0" + state = hass.states.get("sensor.home_wind_day_0d") + assert state + assert state.state == "13.0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get("direction") == "SSE" + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_day_0d") + assert entry + assert entry.unique_id == "0123456-windday-0" + + state = hass.states.get("sensor.home_wind_night_0d") + assert state + assert state.state == "7.4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get("direction") == "WNW" + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_night_0d") + assert entry + assert entry.unique_id == "0123456-windnight-0" + state = hass.states.get("sensor.home_wind_gust_day_0d") assert state assert state.state == "29.6" From 9de393d116bd2686d9d2e5df066848ffb0c1b8ab Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Sat, 19 Dec 2020 09:35:47 -0600 Subject: [PATCH 178/302] Convert zerproc to use new upstream async api (#44357) --- .../components/zerproc/config_flow.py | 2 +- homeassistant/components/zerproc/light.py | 56 ++++++++----------- .../components/zerproc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zerproc/test_light.py | 10 ++-- 6 files changed, 32 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index 28597b3859..6e3d70b081 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass) -> bool: """Return if there are devices that can be discovered.""" try: - devices = await hass.async_add_executor_job(pyzerproc.discover) + devices = await pyzerproc.discover() return len(devices) > 0 except pyzerproc.ZerprocException: _LOGGER.error("Unable to discover nearby Zerproc devices", exc_info=True) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index b45ca4497a..ed44f1aa72 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -28,25 +28,10 @@ SUPPORT_ZERPROC = SUPPORT_BRIGHTNESS | SUPPORT_COLOR DISCOVERY_INTERVAL = timedelta(seconds=60) -PARALLEL_UPDATES = 0 - -def connect_lights(lights: List[pyzerproc.Light]) -> List[pyzerproc.Light]: - """Attempt to connect to lights, and return the connected lights.""" - connected = [] - for light in lights: - try: - light.connect() - connected.append(light) - except pyzerproc.ZerprocException: - _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) - - return connected - - -def discover_entities(hass: HomeAssistant) -> List[Entity]: +async def discover_entities(hass: HomeAssistant) -> List[Entity]: """Attempt to discover new lights.""" - lights = pyzerproc.discover() + lights = await pyzerproc.discover() # Filter out already discovered lights new_lights = [ @@ -54,8 +39,13 @@ def discover_entities(hass: HomeAssistant) -> List[Entity]: ] entities = [] - for light in connect_lights(new_lights): - # Double-check the light hasn't been added in another thread + for light in new_lights: + try: + await light.connect() + except pyzerproc.ZerprocException: + _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) + continue + # Double-check the light hasn't been added in the meantime if light.address not in hass.data[DOMAIN]["addresses"]: hass.data[DOMAIN]["addresses"].add(light.address) entities.append(ZerprocLight(light)) @@ -68,7 +58,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: Callable[[List[Entity], bool], None], ) -> None: - """Set up Abode light devices.""" + """Set up Zerproc light devices.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} if "addresses" not in hass.data[DOMAIN]: @@ -80,7 +70,7 @@ async def async_setup_entry( """Wrap discovery to include params.""" nonlocal warned try: - entities = await hass.async_add_executor_job(discover_entities, hass) + entities = await discover_entities(hass) async_add_entities(entities, update_before_add=True) warned = False except pyzerproc.ZerprocException: @@ -117,11 +107,11 @@ class ZerprocLight(LightEntity): async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" - await self.hass.async_add_executor_job(self._light.disconnect) + await self._light.disconnect() - def on_hass_shutdown(self, event): + async def on_hass_shutdown(self, event): """Execute when Home Assistant is shutting down.""" - self._light.disconnect() + await self._light.disconnect() @property def name(self): @@ -172,7 +162,7 @@ class ZerprocLight(LightEntity): """Return True if entity is available.""" return self._available - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: default_hs = (0, 0) if self._hs_color is None else self._hs_color @@ -182,20 +172,20 @@ class ZerprocLight(LightEntity): brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) - self._light.set_color(*rgb) + await self._light.set_color(*rgb) else: - self._light.turn_on() + await self._light.turn_on() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._light.turn_off() + await self._light.turn_off() - def update(self): + async def async_update(self): """Fetch new state data for this light.""" try: - if not self._light.connected: - self._light.connect() - state = self._light.get_state() + if not await self._light.is_connected(): + await self._light.connect() + state = await self._light.get_state() except pyzerproc.ZerprocException: if self._available: _LOGGER.warning("Unable to connect to %s", self.entity_id) diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index 344ea56910..d5a61fd18a 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": [ - "pyzerproc==0.3.0" + "pyzerproc==0.4.3" ], "codeowners": [ "@emlove" diff --git a/requirements_all.txt b/requirements_all.txt index 411b782577..91c5de815b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1904,7 +1904,7 @@ pyxeoma==1.4.1 pyzbar==0.1.7 # homeassistant.components.zerproc -pyzerproc==0.3.0 +pyzerproc==0.4.3 # homeassistant.components.qnap qnapstats==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e22f5503fc..1e9eb6842a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ pywemo==0.5.3 pywilight==0.0.65 # homeassistant.components.zerproc -pyzerproc==0.3.0 +pyzerproc==0.4.3 # homeassistant.components.rachio rachiopy==1.0.3 diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 14756f1183..3649c954b5 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -44,7 +44,7 @@ async def mock_light(hass, mock_entry): light = MagicMock(spec=pyzerproc.Light) light.address = "AA:BB:CC:DD:EE:FF" light.name = "LEDBlue-CCDDEEFF" - light.connected = False + light.is_connected.return_value = False mock_state = pyzerproc.LightState(False, (0, 0, 0)) @@ -57,7 +57,7 @@ async def mock_light(hass, mock_entry): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - light.connected = True + light.is_connected.return_value = True return light @@ -71,12 +71,12 @@ async def test_init(hass, mock_entry): mock_light_1 = MagicMock(spec=pyzerproc.Light) mock_light_1.address = "AA:BB:CC:DD:EE:FF" mock_light_1.name = "LEDBlue-CCDDEEFF" - mock_light_1.connected = True + mock_light_1.is_connected.return_value = True mock_light_2 = MagicMock(spec=pyzerproc.Light) mock_light_2.address = "11:22:33:44:55:66" mock_light_2.name = "LEDBlue-33445566" - mock_light_2.connected = True + mock_light_2.is_connected.return_value = True mock_state_1 = pyzerproc.LightState(False, (0, 0, 0)) mock_state_2 = pyzerproc.LightState(True, (0, 80, 255)) @@ -144,7 +144,7 @@ async def test_connect_exception(hass, mock_entry): mock_light = MagicMock(spec=pyzerproc.Light) mock_light.address = "AA:BB:CC:DD:EE:FF" mock_light.name = "LEDBlue-CCDDEEFF" - mock_light.connected = False + mock_light.is_connected.return_value = False with patch( "homeassistant.components.zerproc.light.pyzerproc.discover", From a7513c9c13a6df36c0aaf05aa4ef83042fd0ac71 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 19 Dec 2020 07:52:44 -0800 Subject: [PATCH 179/302] Strip "adb shell " prefix in `androidtv.adb_command` service (#44225) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 6adea1af5a..ffcaedeb5a 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.56", + "androidtv[async]==0.0.57", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 91c5de815b..6ecf92e062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -245,7 +245,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.56 +androidtv[async]==0.0.57 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e9eb6842a..77351567f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ airly==1.0.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.56 +androidtv[async]==0.0.57 # homeassistant.components.apns apns2==0.3.0 From 3ac9ead850e4edc23d2a30b48f66eaccd5a683c2 Mon Sep 17 00:00:00 2001 From: Doug Hoffman Date: Sat, 19 Dec 2020 11:30:06 -0500 Subject: [PATCH 180/302] Bump venstarcolortouch to 0.13 (#44373) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index d9de9b9d55..68f762a54f 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -2,6 +2,6 @@ "domain": "venstar", "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.12"], + "requirements": ["venstarcolortouch==0.13"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ecf92e062..6b4b5d676e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2245,7 +2245,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.4.0 # homeassistant.components.venstar -venstarcolortouch==0.12 +venstarcolortouch==0.13 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From ccbf857266880a3049697fecf8ecae1a22c12358 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 19 Dec 2020 17:30:45 +0100 Subject: [PATCH 181/302] Clean Airly config flow (#44352) --- homeassistant/components/airly/config_flow.py | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index f745c75689..58d6a4295e 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -15,11 +15,7 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import ( # pylint:disable=unused-import - DEFAULT_NAME, - DOMAIN, - NO_AIRLY_SENSORS, -) +from .const import DOMAIN, NO_AIRLY_SENSORS # pylint:disable=unused-import class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -28,13 +24,9 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize.""" - self._errors = {} - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - self._errors = {} + errors = {} websession = async_get_clientsession(self.hass) @@ -52,40 +44,33 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except AirlyError as err: if err.status_code == HTTP_UNAUTHORIZED: - self._errors["base"] = "invalid_api_key" + errors["base"] = "invalid_api_key" else: if not location_valid: - self._errors["base"] = "wrong_location" + errors["base"] = "wrong_location" - if not self._errors: + if not errors: return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) - return self._show_config_form( - name=DEFAULT_NAME, - api_key="", - latitude=self.hass.config.latitude, - longitude=self.hass.config.longitude, - ) - - def _show_config_form(self, name=None, api_key=None, latitude=None, longitude=None): - """Show the configuration form to edit data.""" return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_API_KEY, default=api_key): str, + vol.Required(CONF_API_KEY): str, vol.Optional( CONF_LATITUDE, default=self.hass.config.latitude ): cv.latitude, vol.Optional( CONF_LONGITUDE, default=self.hass.config.longitude ): cv.longitude, - vol.Optional(CONF_NAME, default=name): str, + vol.Optional( + CONF_NAME, default=self.hass.config.location_name + ): str, } ), - errors=self._errors, + errors=errors, ) From b7d4c1826c7ab70c92d1397ac47889e1683ecc32 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 19 Dec 2020 16:51:24 +0000 Subject: [PATCH 182/302] Add filter sensor device class from source entity (#44304) Co-authored-by: Martin Hjelmare --- homeassistant/components/filter/sensor.py | 30 ++++++++- tests/components/filter/test_sensor.py | 80 +++++++++++++++++++---- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index f1b873d58a..72300c1621 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -11,8 +11,14 @@ from typing import Optional import voluptuous as vol from homeassistant.components import history -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, + DOMAIN as SENSOR_DOMAIN, + PLATFORM_SCHEMA, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, @@ -132,7 +138,9 @@ FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): vol.Any( + cv.entity_domain(SENSOR_DOMAIN), cv.entity_domain(BINARY_SENSOR_DOMAIN) + ), vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_FILTERS): vol.All( cv.ensure_list, @@ -178,16 +186,20 @@ class SensorFilter(Entity): self._state = None self._filters = filters self._icon = None + self._device_class = None @callback def _update_filter_sensor_state_event(self, event): """Handle device state changes.""" + _LOGGER.debug("Update filter on event: %s", event) self._update_filter_sensor_state(event.data.get("new_state")) @callback def _update_filter_sensor_state(self, new_state, update_ha=True): """Process device state changes.""" if new_state is None or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + self._state = new_state.state + self.async_write_ha_state() return temp_state = new_state @@ -214,6 +226,12 @@ class SensorFilter(Entity): if self._icon is None: self._icon = new_state.attributes.get(ATTR_ICON, ICON) + if ( + self._device_class is None + and new_state.attributes.get(ATTR_DEVICE_CLASS) in SENSOR_DEVICE_CLASSES + ): + self._device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) + if self._unit_of_measurement is None: self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT @@ -283,7 +301,8 @@ class SensorFilter(Entity): # Replay history through the filter chain for state in history_list: - self._update_filter_sensor_state(state, False) + if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]: + self._update_filter_sensor_state(state, False) self.async_on_remove( async_track_state_change_event( @@ -321,6 +340,11 @@ class SensorFilter(Entity): """Return the state attributes of the sensor.""" return {ATTR_ENTITY_ID: self._entity} + @property + def device_class(self): + """Return device class.""" + return self._device_class + class FilterState: """State abstraction for filter usage.""" diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 454fcc976f..8b779696a7 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -14,7 +14,8 @@ from homeassistant.components.filter.sensor import ( TimeSMAFilter, TimeThrottleFilter, ) -from homeassistant.const import SERVICE_RELOAD +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import SERVICE_RELOAD, STATE_UNAVAILABLE import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -35,12 +36,6 @@ def values(): return values -async def init_recorder(hass): - """Init the recorder for testing.""" - await async_init_recorder_component(hass) - await hass.async_start() - - async def test_setup_fail(hass): """Test if filter doesn't exist.""" config = { @@ -50,7 +45,6 @@ async def test_setup_fail(hass): "filters": [{"filter": "nonexisting"}], } } - hass.config.components.add("history") with assert_setup_component(0): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -70,7 +64,8 @@ async def test_chain(hass, values): ], } } - hass.config.components.add("history") + await async_init_recorder_component(hass) + with assert_setup_component(1, "sensor"): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -85,7 +80,6 @@ async def test_chain(hass, values): async def test_chain_history(hass, values, missing=False): """Test if filter chaining works.""" - await init_recorder(hass) config = { "history": {}, "sensor": { @@ -99,6 +93,9 @@ async def test_chain_history(hass, values, missing=False): ], }, } + await async_init_recorder_component(hass) + assert_setup_component(1, "history") + t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) t_2 = dt_util.utcnow() - timedelta(minutes=3) @@ -146,7 +143,6 @@ async def test_chain_history_missing(hass, values): async def test_history_time(hass): """Test loading from history based on a time window.""" - await init_recorder(hass) config = { "history": {}, "sensor": { @@ -156,6 +152,9 @@ async def test_history_time(hass): "filters": [{"filter": "time_throttle", "window_size": "00:01"}], }, } + await async_init_recorder_component(hass) + assert_setup_component(1, "history") + t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) t_2 = dt_util.utcnow() - timedelta(minutes=3) @@ -184,6 +183,63 @@ async def test_history_time(hass): assert "18.0" == state.state +async def test_setup(hass): + """Test if filter attributes are inherited.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + 1, + {"icon": "mdi:test", "device_class": DEVICE_CLASS_TEMPERATURE}, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state.attributes["icon"] == "mdi:test" + assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + assert state.state == "1.0" + + +async def test_invalid_state(hass): + """Test if filter attributes are inherited.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + + async def test_outlier(values): """Test if outlier filter works.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) @@ -316,7 +372,7 @@ def test_time_sma(values): async def test_reload(hass): """Verify we can reload filter sensors.""" - await init_recorder(hass) + await async_init_recorder_component(hass) hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component( From af6dd698c9efa087f4fe0d8829dc56872264cf83 Mon Sep 17 00:00:00 2001 From: eyager1 <44526531+eyager1@users.noreply.github.com> Date: Sat, 19 Dec 2020 12:21:05 -0500 Subject: [PATCH 183/302] Set amazon polly network timeout settings (#44185) * Change network timeout settings * Change network timeout settings --- homeassistant/components/amazon_polly/tts.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3492518421..d1c12e657f 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -2,6 +2,7 @@ import logging import boto3 +import botocore import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider @@ -125,6 +126,10 @@ DEFAULT_TEXT_TYPE = "text" DEFAULT_SAMPLE_RATES = {"mp3": "22050", "ogg_vorbis": "22050", "pcm": "16000"} +AWS_CONF_CONNECT_TIMEOUT = 10 +AWS_CONF_READ_TIMEOUT = 5 +AWS_CONF_MAX_POOL_CONNECTIONS = 1 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), @@ -167,6 +172,11 @@ def get_engine(hass, config, discovery_info=None): CONF_REGION: config[CONF_REGION], CONF_ACCESS_KEY_ID: config.get(CONF_ACCESS_KEY_ID), CONF_SECRET_ACCESS_KEY: config.get(CONF_SECRET_ACCESS_KEY), + "config": botocore.config.Config( + connect_timeout=AWS_CONF_CONNECT_TIMEOUT, + read_timeout=AWS_CONF_READ_TIMEOUT, + max_pool_connections=AWS_CONF_MAX_POOL_CONNECTIONS, + ), } del config[CONF_REGION] @@ -229,6 +239,7 @@ class AmazonPollyProvider(Provider): _LOGGER.error("%s does not support the %s language", voice_id, language) return None, None + _LOGGER.debug("Requesting TTS file for text: %s", message) resp = self.client.synthesize_speech( Engine=self.config[CONF_ENGINE], OutputFormat=self.config[CONF_OUTPUT_FORMAT], @@ -238,6 +249,7 @@ class AmazonPollyProvider(Provider): VoiceId=voice_id, ) + _LOGGER.debug("Reply received for TTS: %s", message) return ( CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")], resp.get("AudioStream").read(), From fbc695e5cf3a2f7a73f6f5a7682b216a5e72231d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 19 Dec 2020 10:22:34 -0700 Subject: [PATCH 184/302] Fix setup of SimpliSafe options flow test (#44375) --- tests/components/simplisafe/test_config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index d4ba26bd48..ec7ad592f1 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -72,6 +72,7 @@ async def test_options_flow(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): + await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From 60ecc8282c8834bc942f14e54d20af7d2b9504f3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 19 Dec 2020 10:29:37 -0700 Subject: [PATCH 185/302] Add options flow for Recollect Waste (#44234) * Add options flow for Recollect Waste * Add test * Typing * Typing * Typing AGAIN * Add missing type hints * Code review * Code review * Don't need to block until done --- .../components/recollect_waste/__init__.py | 15 +++++++- .../components/recollect_waste/config_flow.py | 37 +++++++++++++++++++ .../components/recollect_waste/sensor.py | 35 +++++++++++++----- .../components/recollect_waste/strings.json | 10 +++++ .../recollect_waste/translations/en.json | 10 +++++ .../recollect_waste/test_config_flow.py | 25 +++++++++++++ 6 files changed, 121 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 10d39c1fc4..0600d73d8a 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -14,6 +14,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER +DATA_LISTENER = "listener" + DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) @@ -22,7 +24,7 @@ PLATFORMS = ["sensor"] async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the RainMachine component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} + hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}} return True @@ -64,9 +66,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_forward_entry_setup(entry, component) ) + hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( + async_reload_entry + ) + return True +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Handle an options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" unload_ok = all( @@ -79,5 +90,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) + cancel_listener() return unload_ok diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index 402c143706..8e208f57cc 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -1,9 +1,13 @@ """Config flow for ReCollect Waste integration.""" +from typing import Optional + from aiorecollect.client import Client from aiorecollect.errors import RecollectError import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import ( # pylint:disable=unused-import @@ -24,6 +28,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Define the config flow to handle options.""" + return RecollectWasteOptionsFlowHandler(config_entry) + async def async_step_import(self, import_config: dict = None) -> dict: """Handle configuration via YAML import.""" return await self.async_step_user(import_config) @@ -62,3 +74,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_SERVICE_ID: user_input[CONF_SERVICE_ID], }, ) + + +class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a Recollect Waste options flow.""" + + def __init__(self, entry: config_entries.ConfigEntry): + """Initialize.""" + self._entry = entry + + async def async_step_init(self, user_input: Optional[dict] = None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_FRIENDLY_NAME, + default=self._entry.options.get(CONF_FRIENDLY_NAME), + ): bool + } + ), + ) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 405d66989b..d66c2aae0e 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,11 +1,12 @@ """Support for ReCollect Waste sensors.""" -from typing import Callable +from typing import Callable, List +from aiorecollect.client import PickupType import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.update_coordinator import ( @@ -35,13 +36,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +@callback +def async_get_pickup_type_names( + entry: ConfigEntry, pickup_types: List[PickupType] +) -> List[str]: + """Return proper pickup type names from their associated objects.""" + return [ + t.friendly_name + if entry.options.get(CONF_FRIENDLY_NAME) and t.friendly_name + else t.name + for t in pickup_types + ] + + async def async_setup_platform( hass: HomeAssistant, config: dict, async_add_entities: Callable, discovery_info: dict = None, ): - """Import Awair configuration from YAML.""" + """Import Recollect Waste configuration from YAML.""" LOGGER.warning( "Loading ReCollect Waste via platform setup is deprecated. " "Please remove it from your configuration." @@ -70,8 +84,7 @@ class ReCollectWasteSensor(CoordinatorEntity): """Initialize the sensor.""" super().__init__(coordinator) self._attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._place_id = entry.data[CONF_PLACE_ID] - self._service_id = entry.data[CONF_SERVICE_ID] + self._entry = entry self._state = None @property @@ -97,7 +110,7 @@ class ReCollectWasteSensor(CoordinatorEntity): @property def unique_id(self) -> str: """Return a unique ID.""" - return f"{self._place_id}{self._service_id}" + return f"{self._entry.data[CONF_PLACE_ID]}{self._entry.data[CONF_SERVICE_ID]}" @callback def _handle_coordinator_update(self) -> None: @@ -120,11 +133,13 @@ class ReCollectWasteSensor(CoordinatorEntity): self._state = pickup_event.date self._attributes.update( { - ATTR_PICKUP_TYPES: [t.name for t in pickup_event.pickup_types], + ATTR_PICKUP_TYPES: async_get_pickup_type_names( + self._entry, pickup_event.pickup_types + ), ATTR_AREA_NAME: pickup_event.area_name, - ATTR_NEXT_PICKUP_TYPES: [ - t.name for t in next_pickup_event.pickup_types - ], + ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( + self._entry, next_pickup_event.pickup_types + ), ATTR_NEXT_PICKUP_DATE: next_date, } ) diff --git a/homeassistant/components/recollect_waste/strings.json b/homeassistant/components/recollect_waste/strings.json index 0cd251c737..a350b9880f 100644 --- a/homeassistant/components/recollect_waste/strings.json +++ b/homeassistant/components/recollect_waste/strings.json @@ -14,5 +14,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "title": "Configure Recollect Waste", + "data": { + "friendly_name": "Use friendly names for pickup types (when possible)" + } + } + } } } diff --git a/homeassistant/components/recollect_waste/translations/en.json b/homeassistant/components/recollect_waste/translations/en.json index 28d73d189b..e9deabec71 100644 --- a/homeassistant/components/recollect_waste/translations/en.json +++ b/homeassistant/components/recollect_waste/translations/en.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Use friendly names for pickup types (when possible)" + }, + "title": "Configure Recollect Waste" + } + } } } \ No newline at end of file diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index 1f17e1f60d..ca3fbe8be5 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant.components.recollect_waste import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_FRIENDLY_NAME from tests.async_mock import patch from tests.common import MockConfigEntry @@ -45,6 +46,30 @@ async def test_invalid_place_or_service_id(hass): assert result["errors"] == {"base": "invalid_place_or_service_id"} +async def test_options_flow(hass): + """Test config flow options.""" + conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + config_entry = MockConfigEntry(domain=DOMAIN, unique_id="12345, 12345", data=conf) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.recollect_waste.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_FRIENDLY_NAME: True} + + async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( From 4f088dd77a22a9fb061f960d55b79d0172e638b1 Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Sat, 19 Dec 2020 13:31:45 -0600 Subject: [PATCH 186/302] Connect concurrently to discovered Zerproc lights (#44376) * Connect concurrently to discovered Zerproc lights * Add return type to connect_light Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/zerproc/light.py | 21 +++++++++++++++------ tests/components/zerproc/test_light.py | 22 ++++++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index ed44f1aa72..fc1f0c9a70 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -1,4 +1,5 @@ """Zerproc light platform.""" +import asyncio from datetime import timedelta import logging from typing import Callable, List, Optional @@ -29,6 +30,16 @@ SUPPORT_ZERPROC = SUPPORT_BRIGHTNESS | SUPPORT_COLOR DISCOVERY_INTERVAL = timedelta(seconds=60) +async def connect_light(light: pyzerproc.Light) -> Optional[pyzerproc.Light]: + """Return the given light if it connects successfully.""" + try: + await light.connect() + except pyzerproc.ZerprocException: + _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) + return None + return light + + async def discover_entities(hass: HomeAssistant) -> List[Entity]: """Attempt to discover new lights.""" lights = await pyzerproc.discover() @@ -39,12 +50,10 @@ async def discover_entities(hass: HomeAssistant) -> List[Entity]: ] entities = [] - for light in new_lights: - try: - await light.connect() - except pyzerproc.ZerprocException: - _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) - continue + connected_lights = filter( + None, await asyncio.gather(*(connect_light(light) for light in new_lights)) + ) + for light in connected_lights: # Double-check the light hasn't been added in the meantime if light.address not in hass.data[DOMAIN]["addresses"]: hass.data[DOMAIN]["addresses"].add(light.address) diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 3649c954b5..92d8d2638b 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -141,22 +141,28 @@ async def test_connect_exception(hass, mock_entry): mock_entry.add_to_hass(hass) - mock_light = MagicMock(spec=pyzerproc.Light) - mock_light.address = "AA:BB:CC:DD:EE:FF" - mock_light.name = "LEDBlue-CCDDEEFF" - mock_light.is_connected.return_value = False + mock_light_1 = MagicMock(spec=pyzerproc.Light) + mock_light_1.address = "AA:BB:CC:DD:EE:FF" + mock_light_1.name = "LEDBlue-CCDDEEFF" + mock_light_1.is_connected.return_value = False + + mock_light_2 = MagicMock(spec=pyzerproc.Light) + mock_light_2.address = "11:22:33:44:55:66" + mock_light_2.name = "LEDBlue-33445566" + mock_light_2.is_connected.return_value = False with patch( "homeassistant.components.zerproc.light.pyzerproc.discover", - return_value=[mock_light], + return_value=[mock_light_1, mock_light_2], ), patch.object( - mock_light, "connect", side_effect=pyzerproc.ZerprocException("TEST") + mock_light_1, "connect", side_effect=pyzerproc.ZerprocException("TEST") ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - # The exception should be captured and no entities should be added - assert len(hass.data[DOMAIN]["addresses"]) == 0 + # The exception connecting to light 1 should be captured, but light 2 + # should still be added + assert len(hass.data[DOMAIN]["addresses"]) == 1 async def test_remove_entry(hass, mock_light, mock_entry): From 00c0e9b8da1405a159365e115915f6b8b9440ff1 Mon Sep 17 00:00:00 2001 From: Sjack-Sch <60797694+Sjack-Sch@users.noreply.github.com> Date: Sat, 19 Dec 2020 23:40:44 +0100 Subject: [PATCH 187/302] Home connect functional and ambient light added (#44091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update nl.json * added period to end of every logging entry. * Functional and ambient light added * Change to dict.get method * Update light.py * Update __init__.py Platforms sorted 🔡. * Update api.py - Removed all none light platform related changes. - Period removed from loggin. - Storing entities removed. - Not needed formating change reverted. - * Update const.py All words seperated with underscore. * Update nl.json Reverted change on translation file. * Update light.py -All words of constants seperated with underscore. - f-string used iso concatenation. - Added "ambient"to loggin text. - Removed self._state = false when color setting did not succeed. - Logging starts with a capital. * Update api.py - Removed ending perio in logging - Reverted formating - Removed self.device.entities.append(self) - * Update entity.py - Removed self.device.entities.append(self) * Update api.py - Adding newline at end of file - Added whitespave after "," * Update api.py Newline at end of file * Update const.py Removed unused. * Update light.py - seperated words with whitespaces - improved debug text * Update light.py remove state setting after an error --- .../components/home_connect/__init__.py | 2 +- homeassistant/components/home_connect/api.py | 36 +++- .../components/home_connect/const.py | 12 ++ .../components/home_connect/light.py | 203 ++++++++++++++++++ 4 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/home_connect/light.py diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 38f487a98a..301bd1976e 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index e1ee75297f..8db8afa3a6 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -168,6 +168,30 @@ class DeviceWithDoor(HomeConnectDevice): } +class DeviceWithLight(HomeConnectDevice): + """Device that has lighting.""" + + def get_light_entity(self): + """Get a dictionary with info about the lighting.""" + return { + "device": self, + "desc": "Light", + "ambient": None, + } + + +class DeviceWithAmbientLight(HomeConnectDevice): + """Device that has ambient lighting.""" + + def get_ambientlight_entity(self): + """Get a dictionary with info about the ambient lighting.""" + return { + "device": self, + "desc": "AmbientLight", + "ambient": True, + } + + class Dryer(DeviceWithDoor, DeviceWithPrograms): """Dryer class.""" @@ -202,7 +226,7 @@ class Dryer(DeviceWithDoor, DeviceWithPrograms): } -class Dishwasher(DeviceWithDoor, DeviceWithPrograms): +class Dishwasher(DeviceWithDoor, DeviceWithAmbientLight, DeviceWithPrograms): """Dishwasher class.""" PROGRAMS = [ @@ -335,7 +359,7 @@ class CoffeeMaker(DeviceWithPrograms): return {"switch": program_switches, "sensor": program_sensors} -class Hood(DeviceWithPrograms): +class Hood(DeviceWithLight, DeviceWithAmbientLight, DeviceWithPrograms): """Hood class.""" PROGRAMS = [ @@ -346,9 +370,15 @@ class Hood(DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" + light_entity = self.get_light_entity() + ambientlight_entity = self.get_ambientlight_entity() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() - return {"switch": program_switches, "sensor": program_sensors} + return { + "switch": program_switches, + "sensor": program_sensors, + "light": [light_entity, ambientlight_entity], + } class FridgeFreezer(DeviceWithDoor): diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 10eb5dfd1e..22ce4dba67 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -11,6 +11,18 @@ BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off" BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby" BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" + +COOKING_LIGHTING = "Cooking.Common.Setting.Lighting" +COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness" + +BSH_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled" +BSH_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness" +BSH_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor" +BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR = ( + "BSH.Common.EnumType.AmbientLightColor.CustomColor" +) +BSH_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor" + BSH_DOOR_STATE = "BSH.Common.Status.DoorState" SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py new file mode 100644 index 0000000000..a8d0d7ffbd --- /dev/null +++ b/homeassistant/components/home_connect/light.py @@ -0,0 +1,203 @@ +"""Provides a light for Home Connect.""" +import logging +from math import ceil + +from homeconnect.api import HomeConnectError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + LightEntity, +) +import homeassistant.util.color as color_util + +from .const import ( + BSH_AMBIENT_LIGHT_BRIGHTNESS, + BSH_AMBIENT_LIGHT_COLOR, + BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, + BSH_AMBIENT_LIGHT_CUSTOM_COLOR, + BSH_AMBIENT_LIGHT_ENABLED, + COOKING_LIGHTING, + COOKING_LIGHTING_BRIGHTNESS, + DOMAIN, +) +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect light.""" + + def get_entities(): + """Get a list of entities.""" + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("light", []) + entity_list = [HomeConnectLight(**d) for d in entity_dicts] + entities += entity_list + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectLight(HomeConnectEntity, LightEntity): + """Light for Home Connect.""" + + def __init__(self, device, desc, ambient): + """Initialize the entity.""" + super().__init__(device, desc) + self._state = None + self._brightness = None + self._hs_color = None + self._ambient = ambient + if self._ambient: + self._brightness_key = BSH_AMBIENT_LIGHT_BRIGHTNESS + self._key = BSH_AMBIENT_LIGHT_ENABLED + self._custom_color_key = BSH_AMBIENT_LIGHT_CUSTOM_COLOR + self._color_key = BSH_AMBIENT_LIGHT_COLOR + else: + self._brightness_key = COOKING_LIGHTING_BRIGHTNESS + self._key = COOKING_LIGHTING + self._custom_color_key = None + self._color_key = None + + @property + def is_on(self): + """Return true if the light is on.""" + return bool(self._state) + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the color property.""" + return self._hs_color + + @property + def supported_features(self): + """Flag supported features.""" + if self._ambient: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs): + """Switch the light on, change brightness, change color.""" + if self._ambient: + if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._color_key, + BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying selecting customcolor: %s", err) + if self._brightness is not None: + brightness = 10 + ceil(self._brightness / 255 * 90) + if ATTR_BRIGHTNESS in kwargs: + brightness = 10 + ceil(kwargs[ATTR_BRIGHTNESS] / 255 * 90) + + hs_color = kwargs.get(ATTR_HS_COLOR, default=self._hs_color) + + if hs_color is not None: + rgb = color_util.color_hsv_to_RGB(*hs_color, brightness) + hex_val = color_util.color_rgb_to_hex(rgb[0], rgb[1], rgb[2]) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._custom_color_key, + f"#{hex_val}", + ) + except HomeConnectError as err: + _LOGGER.error( + "Error while trying setting the color: %s", err + ) + else: + _LOGGER.debug("Switching ambient light on for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + True, + ) + except HomeConnectError as err: + _LOGGER.error( + "Error while trying to turn on ambient light: %s", err + ) + + elif ATTR_BRIGHTNESS in kwargs: + _LOGGER.debug("Changing brightness for: %s", self.name) + brightness = 10 + ceil(kwargs[ATTR_BRIGHTNESS] / 255 * 90) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._brightness_key, + brightness, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying set the brightness: %s", err) + else: + _LOGGER.debug("Switching light on for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + True, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn on light: %s", err) + + self.async_entity_update() + + async def async_turn_off(self, **kwargs): + """Switch the light off.""" + _LOGGER.debug("Switching light off for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + False, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn off light: %s", err) + self.async_entity_update() + + async def async_update(self): + """Update the light's status.""" + if self.device.appliance.status.get(self._key, {}).get("value") is True: + self._state = True + elif self.device.appliance.status.get(self._key, {}).get("value") is False: + self._state = False + else: + self._state = None + + _LOGGER.debug("Updated, new light state: %s", self._state) + + if self._ambient: + color = self.device.appliance.status.get(self._custom_color_key, {}) + + if not color: + self._hs_color = None + self._brightness = None + else: + colorvalue = color.get("value")[1:] + rgb = color_util.rgb_hex_to_rgb_list(colorvalue) + hsv = color_util.color_RGB_to_hsv(rgb[0], rgb[1], rgb[2]) + self._hs_color = [hsv[0], hsv[1]] + self._brightness = ceil((hsv[2] - 10) * 255 / 90) + _LOGGER.debug("Updated, new brightness: %s", self._brightness) + + else: + brightness = self.device.appliance.status.get(self._brightness_key, {}) + if brightness is None: + self._brightness = None + else: + self._brightness = ceil((brightness.get("value") - 10) * 255 / 90) + _LOGGER.debug("Updated, new brightness: %s", self._brightness) From 5bdf022bf235e3b09c199c886b1f7fd3485c9015 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 20 Dec 2020 00:04:36 +0000 Subject: [PATCH 188/302] [ci skip] Translation update --- .../components/accuweather/translations/de.json | 3 +++ .../components/adguard/translations/de.json | 3 +++ .../advantage_air/translations/de.json | 3 +++ .../components/agent_dvr/translations/de.json | 3 ++- .../components/airvisual/translations/de.json | 1 + .../alarmdecoder/translations/de.json | 3 +++ .../apple_tv/translations/zh-Hans.json | 15 +++++++++++++++ .../components/arcam_fmj/translations/de.json | 3 ++- .../components/atag/translations/de.json | 3 +++ .../components/aurora/translations/de.json | 5 +++++ .../components/axis/translations/de.json | 3 ++- .../components/axis/translations/zh-Hans.json | 1 + .../azure_devops/translations/de.json | 3 +++ .../components/blink/translations/de.json | 1 + .../components/broadlink/translations/de.json | 8 ++++++-- .../components/brother/translations/de.json | 1 + .../components/bsblan/translations/de.json | 3 +++ .../components/canary/translations/de.json | 6 ++++++ .../components/cloudflare/translations/de.json | 3 +++ .../components/control4/translations/de.json | 4 ++++ .../components/coolmaster/translations/de.json | 1 + .../components/daikin/translations/de.json | 4 +++- .../components/dexcom/translations/de.json | 1 + .../components/elgato/translations/de.json | 6 +++++- .../components/epson/translations/de.json | 3 +++ .../components/flo/translations/de.json | 4 ++++ .../components/flunearyou/translations/de.json | 3 +++ .../components/goalzero/translations/de.json | 4 +++- .../components/guardian/translations/de.json | 3 ++- .../components/heos/translations/de.json | 3 +++ .../components/hlk_sw16/translations/de.json | 3 +++ .../components/huawei_lte/translations/de.json | 3 ++- .../components/humidifier/translations/bg.json | 7 +++++++ .../components/hyperion/translations/pl.json | 3 ++- .../components/kodi/translations/de.json | 6 +++++- .../components/metoffice/translations/de.json | 3 +++ .../motion_blinds/translations/zh-Hans.json | 15 +++++++++++++++ .../components/neato/translations/no.json | 2 +- .../components/neato/translations/pl.json | 17 ++++++++++++++--- .../components/nightscout/translations/de.json | 3 +++ .../components/nws/translations/pl.json | 2 +- .../components/nzbget/translations/de.json | 3 +++ .../components/omnilogic/translations/de.json | 4 ++++ .../openweathermap/translations/de.json | 3 +++ .../components/ovo_energy/translations/de.json | 3 +++ .../panasonic_viera/translations/de.json | 2 ++ .../plum_lightpad/translations/de.json | 3 +++ .../progettihwsw/translations/de.json | 4 ++++ .../components/ps4/translations/de.json | 1 + .../components/ps4/translations/zh-Hans.json | 6 ++++++ .../recollect_waste/translations/et.json | 10 ++++++++++ .../recollect_waste/translations/no.json | 10 ++++++++++ .../components/rfxtrx/translations/de.json | 9 ++++++++- .../components/risco/translations/de.json | 4 ++++ .../components/roon/translations/de.json | 3 ++- .../ruckus_unleashed/translations/de.json | 1 + .../components/samsungtv/translations/de.json | 1 + .../components/sharkiq/translations/de.json | 8 ++++++++ .../components/shelly/translations/de.json | 4 ++++ .../components/simplisafe/translations/de.json | 3 ++- .../components/smappee/translations/de.json | 3 +++ .../smart_meter_texas/translations/de.json | 4 ++++ .../components/sms/translations/de.json | 4 ++++ .../components/squeezebox/translations/de.json | 3 +++ .../synology_dsm/translations/de.json | 1 + .../components/tesla/translations/de.json | 3 +++ .../components/tibber/translations/de.json | 1 + .../components/tuya/translations/de.json | 1 + .../twentemilieu/translations/de.json | 1 + .../components/twinkly/translations/de.json | 3 +++ .../components/upcloud/translations/de.json | 3 +++ .../components/velbus/translations/de.json | 3 +++ .../components/vizio/translations/de.json | 1 + .../components/volumio/translations/de.json | 7 +++++++ 74 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/apple_tv/translations/zh-Hans.json create mode 100644 homeassistant/components/humidifier/translations/bg.json create mode 100644 homeassistant/components/motion_blinds/translations/zh-Hans.json create mode 100644 homeassistant/components/volumio/translations/de.json diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 9c924e7d97..5a13056e68 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 78db52ade4..a02601759b 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -4,6 +4,9 @@ "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "hassio_confirm": { "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?", diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index e2a9646a0a..0d8a005240 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json index d4f8fc4bcc..6ea40d0fd0 100644 --- a/homeassistant/components/agent_dvr/translations/de.json +++ b/homeassistant/components/agent_dvr/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "cannot_connect": "Verbindungsfehler" }, "step": { "user": { diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 7c3467ca55..63012e23da 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert." }, "error": { + "cannot_connect": "Verbindungsfehler", "general_error": "Es gab einen unbekannten Fehler.", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel bereitgestellt." }, diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index c00ee65c27..3f1b7ef816 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json new file mode 100644 index 0000000000..bb1f8e025c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "pair_no_pin": { + "title": "\u914d\u5bf9\u4e2d" + }, + "pair_with_pin": { + "data": { + "pin": "PIN\u7801" + } + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index 05f5615016..92ad0e2266 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindungsfehler" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 2b96bdfa5b..2ced7577fd 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Dieses Ger\u00e4t wurde bereits zu HomeAssistant hinzugef\u00fcgt" }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json index acec471c17..95312fe794 100644 --- a/homeassistant/components/aurora/translations/de.json +++ b/homeassistant/components/aurora/translations/de.json @@ -1,4 +1,9 @@ { + "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index d030914090..4706350cdb 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -7,7 +7,8 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "cannot_connect": "Verbindungsfehler" }, "flow_title": "Achsenger\u00e4t: {name} ({host})", "step": { diff --git a/homeassistant/components/axis/translations/zh-Hans.json b/homeassistant/components/axis/translations/zh-Hans.json index 32d738d838..0ed34907b1 100644 --- a/homeassistant/components/axis/translations/zh-Hans.json +++ b/homeassistant/components/axis/translations/zh-Hans.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "host": "\u4e3b\u673a\u7aef", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index cd849b9f93..1c940ea7a3 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "reauth": { "title": "Erneute Authentifizierung" diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index ec5ad6c53c..f5116110a0 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index e34db6d726..f915040635 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "cannot_connect": "Verbindungsfehler", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", - "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt", + "unknown": "Unerwarteter Fehler" }, "error": { - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse" + "cannot_connect": "Verbindungsfehler", + "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "unknown": "Unerwarteter Fehler" }, "step": { "auth": { diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 4c07d1a299..72bd052cc1 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -5,6 +5,7 @@ "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." }, "error": { + "cannot_connect": "Verbindungsfehler", "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" }, diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 4b4aa37640..5fd61c0bfe 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index 159f961c3a..eebc9bd5fc 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "flow_title": "Canary: {name}", "step": { "user": { diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index 68b1856815..809dad5da4 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unknown": "Unerwarteter Fehler" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_zone": "Ung\u00fcltige Zone" diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json index 1653a11c3e..f9a5783cd9 100644 --- a/homeassistant/components/control4/translations/de.json +++ b/homeassistant/components/control4/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json index 19a57c3180..908dfaa448 100644 --- a/homeassistant/components/coolmaster/translations/de.json +++ b/homeassistant/components/coolmaster/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Verbindungsfehler", "no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden." }, "step": { diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index 1d9ede292f..bbac113eb4 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindungsfehler" }, "error": { + "cannot_connect": "Verbindungsfehler", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index 3b5744ba1a..fadb459a3d 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Konto ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index 4d10424216..7497460445 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert." + "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.", + "cannot_connect": "Verbindungsfehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler" }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json index 82687d50bf..c03615a39f 100644 --- a/homeassistant/components/epson/translations/de.json +++ b/homeassistant/components/epson/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json index 6f39806287..3821567570 100644 --- a/homeassistant/components/flo/translations/de.json +++ b/homeassistant/components/flo/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index e7dc1f6cd2..cd2934170c 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Diese Koordinaten sind bereits registriert." }, + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 80db678f27..d79c03f017 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -1,7 +1,9 @@ { "config": { "error": { - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse" + "cannot_connect": "Verbindungsfehler", + "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "unknown": "Unerwarteter Fehler" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 7407782bc3..27770d690f 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindungsfehler" }, "step": { "user": { diff --git a/homeassistant/components/heos/translations/de.json b/homeassistant/components/heos/translations/de.json index 7c5e1d87c9..92ab6c1c8f 100644 --- a/homeassistant/components/heos/translations/de.json +++ b/homeassistant/components/heos/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json index 6f39806287..94b8d6526d 100644 --- a/homeassistant/components/hlk_sw16/translations/de.json +++ b/homeassistant/components/hlk_sw16/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 451720ffb1..7da997f12d 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -11,7 +11,8 @@ "incorrect_username": "Ung\u00fcltiger Benutzername", "invalid_url": "Ung\u00fcltige URL", "login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuche es sp\u00e4ter erneut", - "response_error": "Unbekannter Fehler vom Ger\u00e4t" + "response_error": "Unbekannter Fehler vom Ger\u00e4t", + "unknown": "Unerwarteter Fehler" }, "flow_title": "Huawei LTE: {name}", "step": { diff --git a/homeassistant/components/humidifier/translations/bg.json b/homeassistant/components/humidifier/translations/bg.json new file mode 100644 index 0000000000..21aa58a9e6 --- /dev/null +++ b/homeassistant/components/humidifier/translations/bg.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json index e705d115c8..33b7c92752 100644 --- a/homeassistant/components/hyperion/translations/pl.json +++ b/homeassistant/components/hyperion/translations/pl.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Nie uda\u0142o si\u0119 uwierzytelni\u0107 przy u\u017cyciu nowo utworzonego tokena", "auth_required_error": "Nie uda\u0142o si\u0119 okre\u015bli\u0107, czy wymagana jest autoryzacja", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "no_id": "Instancja Hyperion Ambilight nie zg\u0142osi\u0142a swojego identyfikatora" + "no_id": "Instancja Hyperion Ambilight nie zg\u0142osi\u0142a swojego identyfikatora", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index f50a90c8a8..a0bf05cb5e 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "unknown": "Unerwarteter Fehler" }, "flow_title": "Kodi: {name}", "step": { diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 0db5c5a422..74c204b968 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Service ist bereits konfiguriert" }, + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/zh-Hans.json b/homeassistant/components/motion_blinds/translations/zh-Hans.json new file mode 100644 index 0000000000..f8dac15948 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "connection_error": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "api_key": "API\u5bc6\u7801", + "host": "IP\u5730\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index dbec3effa1..9db6c2450f 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -33,5 +33,5 @@ } } }, - "title": "Neato Botvac" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index 3b7054ea66..3177ed9d8e 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "create_entry": { - "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + "default": "Pomy\u015blnie uwierzytelniono" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "title": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, "user": { "data": { "password": "Has\u0142o", @@ -22,5 +32,6 @@ "title": "Informacje o koncie Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index a7ad0fe1d2..8581b04099 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Unerwarteter Fehler" + }, "flow_title": "Nightscout", "step": { "user": { diff --git a/homeassistant/components/nws/translations/pl.json b/homeassistant/components/nws/translations/pl.json index 2665a5c5a8..7d0bce9ff1 100644 --- a/homeassistant/components/nws/translations/pl.json +++ b/homeassistant/components/nws/translations/pl.json @@ -15,7 +15,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "station": "Kod stacji METAR" }, - "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte wsp\u00f3\u0142rz\u0119dne geograficzne.", + "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte wsp\u00f3\u0142rz\u0119dne geograficzne. Na razie, kluczem mo\u017ce by\u0107 cokolwiek. Zaleca si\u0119 u\u017cycie prawid\u0142owego adresu e-mail.", "title": "Po\u0142\u0105czenie z National Weather Service" } } diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index 2ebae1083e..018f3870c5 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unknown": "Unerwarteter Fehler" + }, "flow_title": "NZBGet: {name}", "step": { "user": { diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 6f39806287..3821567570 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 35232fe04d..239b47e2d3 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 7ba59cf064..3bd083e483 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/panasonic_viera/translations/de.json b/homeassistant/components/panasonic_viera/translations/de.json index cac04acb87..4b2c14be9d 100644 --- a/homeassistant/components/panasonic_viera/translations/de.json +++ b/homeassistant/components/panasonic_viera/translations/de.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Dieser Panasonic Viera TV ist bereits konfiguriert.", + "cannot_connect": "Verbindungsfehler", "unknown": "Ein unbekannter Fehler ist aufgetreten. Weitere Informationen finden Sie in den Logs." }, "error": { + "cannot_connect": "Verbindungsfehler", "invalid_pin_code": "Der von Ihnen eingegebene PIN-Code war ung\u00fcltig" }, "step": { diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json index f55df964f8..accee16a6f 100644 --- a/homeassistant/components/plum_lightpad/translations/de.json +++ b/homeassistant/components/plum_lightpad/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json index f772a8586d..2e5bed4b66 100644 --- a/homeassistant/components/progettihwsw/translations/de.json +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "relay_modes": { "data": { diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 71dd785b4f..5dd638a717 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -7,6 +7,7 @@ "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { + "cannot_connect": "Verbindungsfehler", "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", "login_failed": "Fehler beim Koppeln mit PlayStation 4. \u00dcberpr\u00fcfe, ob die PIN korrekt ist.", "no_ipaddress": "Gib die IP-Adresse der PlayStation 4 ein, die konfiguriert werden soll." diff --git a/homeassistant/components/ps4/translations/zh-Hans.json b/homeassistant/components/ps4/translations/zh-Hans.json index e2c38ad5d0..3c240d9613 100644 --- a/homeassistant/components/ps4/translations/zh-Hans.json +++ b/homeassistant/components/ps4/translations/zh-Hans.json @@ -24,6 +24,12 @@ }, "description": "\u8f93\u5165\u60a8\u7684 PlayStation 4 \u4fe1\u606f\u3002\u5bf9\u4e8e \"PIN\", \u8bf7\u5bfc\u822a\u5230 PlayStation 4 \u63a7\u5236\u53f0\u4e0a\u7684 \"\u8bbe\u7f6e\"\u3002\u7136\u540e\u5bfc\u822a\u5230 \"\u79fb\u52a8\u5e94\u7528\u8fde\u63a5\u8bbe\u7f6e\", \u7136\u540e\u9009\u62e9 \"\u6dfb\u52a0\u8bbe\u5907\"\u3002\u8f93\u5165\u663e\u793a\u7684 PIN\u3002", "title": "PlayStation 4" + }, + "mode": { + "data": { + "mode": "\u8bbe\u7f6e\u6a21\u5f0f" + }, + "title": "PlayStation 4" } } } diff --git a/homeassistant/components/recollect_waste/translations/et.json b/homeassistant/components/recollect_waste/translations/et.json index e1402d12e4..8bbc0de94b 100644 --- a/homeassistant/components/recollect_waste/translations/et.json +++ b/homeassistant/components/recollect_waste/translations/et.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "S\u00f5bralike nimede kasutamine pr\u00fcgi t\u00fc\u00fcpide puhul (kui see on v\u00f5imalik)" + }, + "title": "Seadista Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/no.json b/homeassistant/components/recollect_waste/translations/no.json index 6c4932505b..eb9f73fef9 100644 --- a/homeassistant/components/recollect_waste/translations/no.json +++ b/homeassistant/components/recollect_waste/translations/no.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Bruk vennlige navn for hentetyper (n\u00e5r det er mulig)" + }, + "title": "Konfigurer Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 6d934ed0e6..1979a10cb8 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich." + "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich.", + "cannot_connect": "Verbindungsfehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler" }, "step": { "setup_network": { @@ -25,6 +29,9 @@ } }, "options": { + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "prompt_options": { "data": { diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index a2c942db57..ad863f7ff7 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 9e6453222a..9918e38670 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -1,7 +1,8 @@ { "config": { "error": { - "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt." + "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.", + "unknown": "Unerwarteter Fehler" } } } \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/de.json b/homeassistant/components/ruckus_unleashed/translations/de.json index 1b5c5cb760..ae15ec058b 100644 --- a/homeassistant/components/ruckus_unleashed/translations/de.json +++ b/homeassistant/components/ruckus_unleashed/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index c083048e4c..e335426763 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -4,6 +4,7 @@ "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 berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", + "cannot_connect": "Verbindungsfehler", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, "flow_title": "Samsung TV: {model}", diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json index 5a1d4f2f18..2294960d6f 100644 --- a/homeassistant/components/sharkiq/translations/de.json +++ b/homeassistant/components/sharkiq/translations/de.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index dac21a6af7..74d0f831c8 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "flow_title": "Shelly: {name}", "step": { "credentials": { diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 67f059c1cd..ab05cf649d 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet." }, "error": { - "identifier_exists": "Konto bereits registriert" + "identifier_exists": "Konto bereits registriert", + "unknown": "Unerwarteter Fehler" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index 0e77c8fbd7..a609492f42 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Verbindungsfehler" + }, "flow_title": "Smappee: {name}", "step": { "environment": { diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json index 6f39806287..3821567570 100644 --- a/homeassistant/components/smart_meter_texas/translations/de.json +++ b/homeassistant/components/smart_meter_texas/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json index 273daf6ef0..1252313a43 100644 --- a/homeassistant/components/sms/translations/de.json +++ b/homeassistant/components/sms/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index 24087b5061..667bf6dbd1 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "edit": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 47c01a0309..303321ea94 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Host bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "missing_data": "Fehlende Daten: Bitte versuchen Sie es sp\u00e4ter noch einmal oder eine andere Konfiguration", "otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code", "unknown": "Unbekannter Fehler: Bitte \u00fcberpr\u00fcfen Sie die Protokolle, um weitere Details zu erhalten" diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index 67ad3cc5e5..09100c355c 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index cd8edbc3e5..670f57df8b 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ein Tibber-Konto ist bereits konfiguriert." }, "error": { + "cannot_connect": "Verbindungsfehler", "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "timeout": "Zeit\u00fcberschreitung beim Verbinden mit Tibber" }, diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index e3183fb5c5..4cdcdfced7 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "Verbindungsfehler", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "flow_title": "Tuya Konfiguration", diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 2ae8c2863a..27ba9bb29c 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Verbindungsfehler", "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden." }, "step": { diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index e702c18e89..2b4c70a0ba 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "title": "Twinkly" diff --git a/homeassistant/components/upcloud/translations/de.json b/homeassistant/components/upcloud/translations/de.json index ffdd1e0dd5..76bbc70569 100644 --- a/homeassistant/components/upcloud/translations/de.json +++ b/homeassistant/components/upcloud/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/velbus/translations/de.json b/homeassistant/components/velbus/translations/de.json index d9013bea39..c6c872c85e 100644 --- a/homeassistant/components/velbus/translations/de.json +++ b/homeassistant/components/velbus/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index f2b24b2c55..ddb68ec09f 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "Verbindungsfehler", "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { diff --git a/homeassistant/components/volumio/translations/de.json b/homeassistant/components/volumio/translations/de.json new file mode 100644 index 0000000000..ef455299de --- /dev/null +++ b/homeassistant/components/volumio/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + } + } +} \ No newline at end of file From 81341bbf91dc50a0dbddf30481a934b4f98c21b2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 19 Dec 2020 16:41:29 -0800 Subject: [PATCH 189/302] Handle expiration of nest auth credentials (#44202) Co-authored-by: Martin Hjelmare --- homeassistant/components/nest/__init__.py | 14 +- homeassistant/components/nest/config_flow.py | 53 +++- homeassistant/components/nest/strings.json | 7 +- tests/components/nest/test_config_flow_sdm.py | 243 ++++++++++++++---- 4 files changed, 262 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 97c9da5794..151b1dac00 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -6,14 +6,14 @@ import logging import threading from google_nest_sdm.event import AsyncEventCallback, EventMessage -from google_nest_sdm.exceptions import GoogleNestException +from google_nest_sdm.exceptions import AuthException, GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from nest import Nest from nest.nest import APIError, AuthorizationError import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, @@ -231,6 +231,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await subscriber.start_async() + except AuthException as err: + _LOGGER.debug("Subscriber authentication error: %s", err) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=entry.data, + ) + ) + return False except GoogleNestException as err: _LOGGER.error("Subscriber error: %s", err) subscriber.stop_async() diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 6aaa5bcc48..36b0da239a 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -75,6 +75,12 @@ class NestFlowHandler( VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + def __init__(self): + """Initialize NestFlowHandler.""" + super().__init__() + # When invoked for reauth, allows updating an existing config entry + self._reauth = False + @classmethod def register_sdm_api(cls, hass): """Configure the flow handler to use the SDM API.""" @@ -103,19 +109,56 @@ class NestFlowHandler( async def async_oauth_create_entry(self, data: dict) -> dict: """Create an entry for the SDM flow.""" + assert self.is_sdm_api(), "Step only supported for SDM API" data[DATA_SDM] = {} + await self.async_set_unique_id(DOMAIN) + # Update existing config entry when in the reauth flow. This + # integration only supports one config entry so remove any prior entries + # added before the "single_instance_allowed" check was added + existing_entries = self.hass.config_entries.async_entries(DOMAIN) + if existing_entries: + updated = False + for entry in existing_entries: + if updated: + await self.hass.config_entries.async_remove(entry.entry_id) + continue + updated = True + self.hass.config_entries.async_update_entry( + entry, data=data, unique_id=DOMAIN + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return await super().async_oauth_create_entry(data) + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + assert self.is_sdm_api(), "Step only supported for SDM API" + self._reauth = True # Forces update of existing config entry + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Confirm reauth dialog.""" + assert self.is_sdm_api(), "Step only supported for SDM API" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self.is_sdm_api(): + # Reauth will update an existing entry + if self.hass.config_entries.async_entries(DOMAIN) and not self._reauth: + return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) return await self.async_step_init(user_input) async def async_step_init(self, user_input=None): """Handle a flow start.""" - if self.is_sdm_api(): - raise UnexpectedStateError("Step only supported for legacy API") + assert not self.is_sdm_api(), "Step only supported for legacy API" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) @@ -145,8 +188,7 @@ class NestFlowHandler( implementation type we expect a pin or an external component to deliver the authentication code. """ - if self.is_sdm_api(): - raise UnexpectedStateError("Step only supported for legacy API") + assert not self.is_sdm_api(), "Step only supported for legacy API" flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] @@ -188,8 +230,7 @@ class NestFlowHandler( async def async_step_import(self, info): """Import existing auth from Nest.""" - if self.is_sdm_api(): - raise UnexpectedStateError("Step only supported for legacy API") + assert not self.is_sdm_api(), "Step only supported for legacy API" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index f945469e26..6ce529621a 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -4,6 +4,10 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nest integration needs to re-authenticate your account" + }, "init": { "title": "Authentication Provider", "description": "[%key:common::config_flow::title::oauth2_pick_implementation%]", @@ -30,7 +34,8 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 6573b17980..e506f269d6 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,9 +1,14 @@ """Test the Google Nest Device Access config flow.""" + +import pytest + from homeassistant import config_entries, setup from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from .common import MockConfigEntry + from tests.async_mock import patch CLIENT_ID = "1234" @@ -11,64 +16,210 @@ CLIENT_SECRET = "5678" PROJECT_ID = "project-id-4321" SUBSCRIBER_ID = "subscriber-id-9876" +CONFIG = { + DOMAIN: { + "project_id": PROJECT_ID, + "subscriber_id": SUBSCRIBER_ID, + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, + "http": {"base_url": "https://example.com"}, +} -async def test_full_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host -): - """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "project_id": PROJECT_ID, - "subscriber_id": SUBSCRIBER_ID, - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, + +def get_config_entry(hass): + """Return a single config entry.""" + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + return entries[0] + + +class OAuthFixture: + """Simulate the oauth flow used by the config flow.""" + + def __init__(self, hass, aiohttp_client, aioclient_mock): + """Initialize OAuthFixture.""" + self.hass = hass + self.aiohttp_client = aiohttp_client + self.aioclient_mock = aioclient_mock + + async def async_oauth_flow(self, result): + """Invoke the oauth flow with fake responses.""" + state = config_entry_oauth2_flow._encode_jwt( + self.hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", }, - "http": {"base_url": "https://example.com"}, - }, - ) + ) + + oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) + assert result["type"] == "external" + assert result["url"] == ( + f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" + "+https://www.googleapis.com/auth/pubsub" + "&access_type=offline&prompt=consent" + ) + + client = await self.aiohttp_client(self.hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + self.aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.nest.async_setup_entry", return_value=True + ) as mock_setup: + await self.hass.config_entries.flow.async_configure(result["flow_id"]) + assert len(mock_setup.mock_calls) == 1 + + +@pytest.fixture +async def oauth(hass, aiohttp_client, aioclient_mock, current_request_with_host): + """Create the simulated oauth flow.""" + return OAuthFixture(hass, aiohttp_client, aioclient_mock) + + +async def test_full_flow(hass, oauth): + """Check full flow.""" + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", + await oauth.async_oauth_flow(result) + + entry = get_config_entry(hass) + assert entry.title == "Configuration.yaml" + assert "token" in entry.data + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_reauth(hass, oauth): + """Test Nest reauthentication.""" + + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + old_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": DOMAIN, + "token": { + # Verify this is replaced at end of the test + "access_token": "some-revoked-token", + }, + "sdm": {}, }, + unique_id=DOMAIN, + ) + old_entry.add_to_hass(hass) + + entry = get_config_entry(hass) + assert entry.data["token"] == { + "access_token": "some-revoked-token", + } + + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data ) - oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) - assert result["url"] == ( - f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" - "+https://www.googleapis.com/auth/pubsub" - "&access_type=offline&prompt=consent" + # Advance through the reauth flow + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + # Run the oauth flow + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + await oauth.async_oauth_flow(result) + + # Verify existing tokens are replaced + entry = get_config_entry(hass) + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_single_config_entry(hass): + """Test that only a single config entry is allowed.""" + old_entry = MockConfigEntry( + domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} ) + old_entry.add_to_hass(hass) - client = await aiohttp_client(hass.http.app) - resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 - assert resp.headers["content-type"] == "text/html; charset=utf-8" + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) - aioclient_mock.post( - OAUTH2_TOKEN, - json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - }, + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" - with patch( - "homeassistant.components.nest.async_setup_entry", return_value=True - ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 +async def test_unexpected_existing_config_entries(hass, oauth): + """Test Nest reauthentication with multiple existing config entries.""" + # Note that this case will not happen in the future since only a single + # instance is now allowed, but this may have been allowed in the past. + # On reauth, only one entry is kept and the others are deleted. + + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + old_entry = MockConfigEntry( + domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + ) + old_entry.add_to_hass(hass) + + old_entry = MockConfigEntry( + domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + ) + old_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + + # Invoke the reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + flows = hass.config_entries.flow.async_progress() + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + await oauth.async_oauth_flow(result) + + # Only a single entry now exists, and the other was cleaned up + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.unique_id == DOMAIN + entry.data["token"].pop("expires_at") + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } From 5dc48df0938a699001c2ca3d6987290b154aad93 Mon Sep 17 00:00:00 2001 From: Arto Jantunen Date: Sun, 20 Dec 2020 05:53:01 +0200 Subject: [PATCH 190/302] Add support for toggling Daikin streamers (#40418) * pydaikin version bump to 2.4.0 Add support for controlling the streamer feature. * Add switch for toggling Daikin streamer on and off * Have DaikinStreamerSwitch inherit from SwitchEntity instead of ToggleEntity --- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/switch.py | 61 ++++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a69871a1ef..ebf31967cc 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.3.1"], + "requirements": ["pydaikin==2.4.0"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum" diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index b39d7f27c5..5e0e1b5761 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -1,9 +1,13 @@ """Support for Daikin AirBase zones.""" +from homeassistant.components.switch import SwitchEntity from homeassistant.helpers.entity import ToggleEntity from . import DOMAIN as DAIKIN_DOMAIN ZONE_ICON = "mdi:home-circle" +STREAMER_ICON = "mdi:air-filter" +DAIKIN_ATTR_ADVANCED = "adv" +DAIKIN_ATTR_STREAMER = "streamer" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -17,15 +21,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] + switches = [] zones = daikin_api.device.zones if zones: - async_add_entities( + switches.extend( [ DaikinZoneSwitch(daikin_api, zone_id) for zone_id, zone in enumerate(zones) if zone != ("-", "0") ] ) + if daikin_api.device.support_advanced_modes: + # It isn't possible to find out from the API responses if a specific + # device supports the streamer, so assume so if it does support + # advanced modes. + switches.append(DaikinStreamerSwitch(daikin_api)) + if switches: + async_add_entities(switches) class DaikinZoneSwitch(ToggleEntity): @@ -72,3 +84,50 @@ class DaikinZoneSwitch(ToggleEntity): async def async_turn_off(self, **kwargs): """Turn the zone off.""" await self._api.device.set_zone(self._zone_id, "0") + + +class DaikinStreamerSwitch(SwitchEntity): + """Streamer state.""" + + def __init__(self, daikin_api): + """Initialize streamer switch.""" + self._api = daikin_api + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._api.device.mac}-streamer" + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return STREAMER_ICON + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._api.name} streamer" + + @property + def is_on(self): + """Return the state of the sensor.""" + return ( + DAIKIN_ATTR_STREAMER in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] + ) + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info + + async def async_update(self): + """Retrieve latest state.""" + await self._api.async_update() + + async def async_turn_on(self, **kwargs): + """Turn the zone on.""" + await self._api.device.set_streamer("on") + + async def async_turn_off(self, **kwargs): + """Turn the zone off.""" + await self._api.device.set_streamer("off") diff --git a/requirements_all.txt b/requirements_all.txt index 6b4b5d676e..dac983b1ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1331,7 +1331,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.3.1 +pydaikin==2.4.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77351567f3..bf0074d319 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -670,7 +670,7 @@ pycoolmasternet-async==0.1.2 pycountry==19.8.18 # homeassistant.components.daikin -pydaikin==2.3.1 +pydaikin==2.4.0 # homeassistant.components.deconz pydeconz==76 From 1ae3bb6af58322e7dadf378ae5ee05a866e17285 Mon Sep 17 00:00:00 2001 From: JJdeVries <43748187+JJdeVries@users.noreply.github.com> Date: Sun, 20 Dec 2020 05:13:52 +0100 Subject: [PATCH 191/302] Add xiamoi_miio the water_box / mop status (#43355) * Adding the water_box / mop status * Clean up Co-authored-by: Teemu R. Co-authored-by: Martin Hjelmare Co-authored-by: Teemu R. --- homeassistant/components/xiaomi_miio/vacuum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 2a9ae1187b..ab76d14a69 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -75,6 +75,7 @@ ATTR_STATUS = "status" ATTR_ZONE_ARRAY = "zone" ATTR_ZONE_REPEATER = "repeats" ATTR_TIMERS = "timers" +ATTR_MOP_ATTACHED = "mop_attached" SUPPORT_XIAOMI = ( SUPPORT_STATE @@ -326,6 +327,7 @@ class MiroboVacuum(StateVacuumEntity): self.consumable_state.sensor_dirty_left.total_seconds() / 3600 ), ATTR_STATUS: str(self.vacuum_state.state), + ATTR_MOP_ATTACHED: self.vacuum_state.is_water_box_attached, } ) From 89fe232643134f283c041537e9f6841f47dc1c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=83=D0=B1=D0=BE=D0=B2=D0=B8=D0=BA=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 20 Dec 2020 06:40:43 +0200 Subject: [PATCH 192/302] Add google cloud tts SSML + fix (#40203) * Add SSML + fix SSML option is added + pitch paramter fix + couple style code changes * Remove redundant .get() * Fix PITCH_SCHEMA, remove redundant .get of dict --- homeassistant/components/google_cloud/tts.py | 47 +++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 1658fcec1f..69b276fc75 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -20,6 +20,7 @@ CONF_SPEED = "speed" CONF_PITCH = "pitch" CONF_GAIN = "gain" CONF_PROFILES = "profiles" +CONF_TEXT_TYPE = "text_type" SUPPORTED_LANGUAGES = [ "ar-XA", @@ -84,6 +85,9 @@ MIN_GAIN = -96.0 MAX_GAIN = 16.0 DEFAULT_GAIN = 0 +SUPPORTED_TEXT_TYPES = ["text", "ssml"] +DEFAULT_TEXT_TYPE = "text" + SUPPORTED_PROFILES = [ "wearable-class-device", "handset-class-device", @@ -103,6 +107,7 @@ SUPPORTED_OPTIONS = [ CONF_PITCH, CONF_GAIN, CONF_PROFILES, + CONF_TEXT_TYPE, ] GENDER_SCHEMA = vol.All( @@ -116,6 +121,7 @@ SPEED_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_SPEED, max=MAX_SPEED PITCH_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_PITCH, max=MAX_PITCH)) GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) PROFILES_SCHEMA = vol.All(cv.ensure_list, [vol.In(SUPPORTED_PROFILES)]) +TEXT_TYPE_SCHEMA = vol.All(vol.Lower, vol.In(SUPPORTED_TEXT_TYPES)) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -128,6 +134,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): PITCH_SCHEMA, vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, + vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): TEXT_TYPE_SCHEMA, } ) @@ -144,14 +151,15 @@ async def async_get_engine(hass, config, discovery_info=None): return GoogleCloudTTSProvider( hass, key_file, - config.get(CONF_LANG), - config.get(CONF_GENDER), - config.get(CONF_VOICE), - config.get(CONF_ENCODING), - config.get(CONF_SPEED), - config.get(CONF_PITCH), - config.get(CONF_GAIN), - config.get(CONF_PROFILES), + config[CONF_LANG], + config[CONF_GENDER], + config[CONF_VOICE], + config[CONF_ENCODING], + config[CONF_SPEED], + config[CONF_PITCH], + config[CONF_GAIN], + config[CONF_PROFILES], + config[CONF_TEXT_TYPE], ) @@ -170,6 +178,7 @@ class GoogleCloudTTSProvider(Provider): pitch=0, gain=0, profiles=None, + text_type=DEFAULT_TEXT_TYPE, ): """Init Google Cloud TTS service.""" self.hass = hass @@ -182,6 +191,7 @@ class GoogleCloudTTSProvider(Provider): self._pitch = pitch self._gain = gain self._profiles = profiles + self._text_type = text_type if key_file: self._client = texttospeech.TextToSpeechClient.from_service_account_json( @@ -216,6 +226,7 @@ class GoogleCloudTTSProvider(Provider): CONF_PITCH: self._pitch, CONF_GAIN: self._gain, CONF_PROFILES: self._profiles, + CONF_TEXT_TYPE: self._text_type, } async def async_get_tts_audio(self, message, language, options=None): @@ -224,11 +235,12 @@ class GoogleCloudTTSProvider(Provider): { vol.Optional(CONF_GENDER, default=self._gender): GENDER_SCHEMA, vol.Optional(CONF_VOICE, default=self._voice): VOICE_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): SCHEMA_ENCODING, + vol.Optional(CONF_ENCODING, default=self._encoding): SCHEMA_ENCODING, vol.Optional(CONF_SPEED, default=self._speed): SPEED_SCHEMA, - vol.Optional(CONF_PITCH, default=self._speed): SPEED_SCHEMA, - vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, - vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, + vol.Optional(CONF_PITCH, default=self._pitch): PITCH_SCHEMA, + vol.Optional(CONF_GAIN, default=self._gain): GAIN_SCHEMA, + vol.Optional(CONF_PROFILES, default=self._profiles): PROFILES_SCHEMA, + vol.Optional(CONF_TEXT_TYPE, default=self._text_type): TEXT_TYPE_SCHEMA, } ) options = options_schema(options) @@ -239,8 +251,9 @@ class GoogleCloudTTSProvider(Provider): language = _voice[:5] try: + params = {options[CONF_TEXT_TYPE]: message} # pylint: disable=no-member - synthesis_input = texttospeech.types.SynthesisInput(text=message) + synthesis_input = texttospeech.types.SynthesisInput(**params) voice = texttospeech.types.VoiceSelectionParams( language_code=language, @@ -250,10 +263,10 @@ class GoogleCloudTTSProvider(Provider): audio_config = texttospeech.types.AudioConfig( audio_encoding=texttospeech.enums.AudioEncoding[_encoding], - speaking_rate=options.get(CONF_SPEED), - pitch=options.get(CONF_PITCH), - volume_gain_db=options.get(CONF_GAIN), - effects_profile_id=options.get(CONF_PROFILES), + speaking_rate=options[CONF_SPEED], + pitch=options[CONF_PITCH], + volume_gain_db=options[CONF_GAIN], + effects_profile_id=options[CONF_PROFILES], ) # pylint: enable=no-member From f3cabe97e0ddcf66fcdc6c98d2b887a6b6408cc6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 21 Dec 2020 00:04:09 +0000 Subject: [PATCH 193/302] [ci skip] Translation update --- .../components/abode/translations/hu.json | 8 ++- .../components/abode/translations/no.json | 2 +- .../components/airvisual/translations/no.json | 4 +- .../components/almond/translations/no.json | 2 +- .../ambiclimate/translations/no.json | 2 +- .../components/apple_tv/translations/hu.json | 29 +++++++- .../components/august/translations/no.json | 2 +- .../components/auth/translations/no.json | 8 +-- .../components/awair/translations/no.json | 2 +- .../azure_devops/translations/no.json | 8 +-- .../components/blink/translations/no.json | 4 +- .../components/broadlink/translations/no.json | 6 +- .../components/cloud/translations/hu.json | 3 + .../components/cloud/translations/no.json | 2 +- .../components/esphome/translations/no.json | 2 +- .../fireservicerota/translations/hu.json | 11 ++++ .../fireservicerota/translations/no.json | 4 +- .../components/hangouts/translations/no.json | 4 +- .../components/hassio/translations/hu.json | 1 + .../home_connect/translations/no.json | 2 +- .../components/hyperion/translations/hu.json | 14 ++++ .../components/hyperion/translations/no.json | 2 +- .../components/icloud/translations/no.json | 4 +- .../logi_circle/translations/no.json | 10 +-- .../mobile_app/translations/hu.json | 5 ++ .../components/mqtt/translations/zh-Hans.json | 6 +- .../components/neato/translations/hu.json | 6 +- .../components/neato/translations/no.json | 6 +- .../components/nest/translations/ca.json | 5 ++ .../components/nest/translations/en.json | 5 ++ .../components/nest/translations/et.json | 5 ++ .../components/nest/translations/hu.json | 15 ++++- .../components/nest/translations/it.json | 5 ++ .../components/nest/translations/no.json | 15 +++-- .../components/nest/translations/ru.json | 5 ++ .../components/netatmo/translations/no.json | 4 +- .../ovo_energy/translations/hu.json | 5 ++ .../ovo_energy/translations/no.json | 4 +- .../components/owntracks/translations/no.json | 2 +- .../components/ozw/translations/hu.json | 3 +- .../components/plex/translations/no.json | 2 +- .../components/plugwise/translations/hu.json | 14 ++++ .../components/point/translations/no.json | 12 ++-- .../recollect_waste/translations/ca.json | 10 +++ .../recollect_waste/translations/it.json | 10 +++ .../recollect_waste/translations/ru.json | 10 +++ .../components/rfxtrx/translations/hu.json | 14 ++++ .../components/ring/translations/no.json | 2 +- .../components/roon/translations/no.json | 4 +- .../components/sharkiq/translations/no.json | 2 +- .../simplisafe/translations/no.json | 8 +-- .../components/smappee/translations/no.json | 4 +- .../smartthings/translations/no.json | 4 +- .../components/solaredge/translations/hu.json | 3 + .../components/soma/translations/no.json | 2 +- .../components/somfy/translations/no.json | 4 +- .../components/sonarr/translations/no.json | 4 +- .../components/spotify/translations/no.json | 6 +- .../srp_energy/translations/hu.json | 11 ++++ .../components/tasmota/translations/hu.json | 9 +++ .../tellduslive/translations/no.json | 8 +-- .../components/toon/translations/no.json | 8 +-- .../components/withings/translations/no.json | 6 +- .../components/xbox/translations/no.json | 4 +- .../components/zha/translations/zh-Hans.json | 66 +++++++++++++++---- .../zoneminder/translations/no.json | 4 +- 66 files changed, 352 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/fireservicerota/translations/hu.json create mode 100644 homeassistant/components/hyperion/translations/hu.json create mode 100644 homeassistant/components/plugwise/translations/hu.json create mode 100644 homeassistant/components/srp_energy/translations/hu.json create mode 100644 homeassistant/components/tasmota/translations/hu.json diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index 77ce53abef..5df508d0f3 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -4,9 +4,15 @@ "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA k\u00f3d (6 jegy\u0171)" + } + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json index c215ec7dae..27706c3d79 100644 --- a/homeassistant/components/abode/translations/no.json +++ b/homeassistant/components/abode/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 138b84f6fd..abf4a9f62e 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Plasseringen er allerede konfigurert eller Node / Pro ID er allerede registrert.", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -31,7 +31,7 @@ "data": { "api_key": "API-n\u00f8kkel" }, - "title": "Autentiser AirVisual p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 0360619394..1b0f03b801 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Tilkobling mislyktes", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json index 88a4a0bdbb..c39aa7637f 100644 --- a/homeassistant/components/ambiclimate/translations/no.json +++ b/homeassistant/components/ambiclimate/translations/no.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Ukjent feil ved oppretting av tilgangstoken.", "already_configured": "Kontoen er allerede konfigurert", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 4eea4abc15..26c02fabbb 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -1,18 +1,41 @@ { "config": { + "abort": { + "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba" + }, "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "invalid_auth": "Azonos\u00edt\u00e1s nem siker\u00fclt", - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba" }, "step": { "confirm": { "title": "Apple TV sikeresen hozz\u00e1adva" }, + "pair_no_pin": { + "title": "P\u00e1ros\u00edt\u00e1s" + }, "pair_with_pin": { "data": { "pin": "PIN K\u00f3d" - } + }, + "title": "P\u00e1ros\u00edt\u00e1s" + }, + "reconfigure": { + "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" + }, + "service_problem": { + "title": "Nem siker\u00fclt hozz\u00e1adni a szolg\u00e1ltat\u00e1st" + }, + "user": { + "data": { + "device_input": "Eszk\u00f6z" + }, + "title": "\u00daj Apple TV be\u00e1ll\u00edt\u00e1sa" } } - } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index 6a418ccdc9..ae314897e7 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/auth/translations/no.json b/homeassistant/components/auth/translations/no.json index ea0f1baa06..c0252e045b 100644 --- a/homeassistant/components/auth/translations/no.json +++ b/homeassistant/components/auth/translations/no.json @@ -2,10 +2,10 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "Ingen varslingstjenester er tilgjengelig." + "no_available_service": "Ingen varslingstjenester er tilgjengelig" }, "error": { - "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen." + "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen" }, "step": { "init": { @@ -25,8 +25,8 @@ }, "step": { "init": { - "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/).\n\n {qr_code} \n \nEtter at du har skannet koden, angir du den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du fylle inn f\u00f8lgende kode manuelt: **`{code}`**.", - "title": "Sett opp tofaktorautentisering ved hjelp av TOTP" + "description": "For \u00e5 aktivere totrinnsbekreftelse ved hjelp av tidsbaserte engangspassord, skann QR-koden med godkjenningsappen din. Hvis du ikke har en, anbefaler vi enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/).\n\n {qr_code} \n \nEtter at du har skannet koden, angir du den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du fylle inn f\u00f8lgende kode manuelt: **`{code}`**.", + "title": "Sett opp totrinnsbekreftelse ved hjelp av TOTP" } }, "title": "" diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 43ffe5960c..98486a28b0 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_access_token": "Ugyldig tilgangstoken", diff --git a/homeassistant/components/azure_devops/translations/no.json b/homeassistant/components/azure_devops/translations/no.json index bc649dcadf..50ee7a7a2a 100644 --- a/homeassistant/components/azure_devops/translations/no.json +++ b/homeassistant/components/azure_devops/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -13,10 +13,10 @@ "step": { "reauth": { "data": { - "personal_access_token": "Token for personlig tilgang (PAT)" + "personal_access_token": "Personlig tilgangstoken (PAT)" }, - "description": "Autentiseringen mislyktes for {project_url} . Vennligst skriv inn gjeldende legitimasjon.", - "title": "Reautorisasjon" + "description": "Autentiseringen mislyktes for {project_url}. Vennligst skriv inn gjeldende legitimasjon.", + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/blink/translations/no.json b/homeassistant/components/blink/translations/no.json index e2be7cf409..0b99005c38 100644 --- a/homeassistant/components/blink/translations/no.json +++ b/homeassistant/components/blink/translations/no.json @@ -12,10 +12,10 @@ "step": { "2fa": { "data": { - "2fa": "To-faktorskode" + "2fa": "Totrinnsbekreftelse kode" }, "description": "Skriv inn pin-koden som ble sendt til din e-posten", - "title": "Totrinnsverifisering" + "title": "Totrinnsbekreftelse" }, "user": { "data": { diff --git a/homeassistant/components/broadlink/translations/no.json b/homeassistant/components/broadlink/translations/no.json index 8c72e9d92f..d64fedecc5 100644 --- a/homeassistant/components/broadlink/translations/no.json +++ b/homeassistant/components/broadlink/translations/no.json @@ -16,7 +16,7 @@ "flow_title": "{name} ({model} p\u00e5 {host})", "step": { "auth": { - "title": "Autentiser til enheten" + "title": "Godkjenning til enheten" }, "finish": { "data": { @@ -25,14 +25,14 @@ "title": "Velg et navn p\u00e5 enheten" }, "reset": { - "description": "{name} ( {model} p\u00e5 {host} ) er l\u00e5st. Du m\u00e5 l\u00e5se opp enheten for \u00e5 autentisere og fullf\u00f8re konfigurasjonen. Bruksanvisning:\n 1. \u00c5pne Broadlink-appen.\n 2. Klikk p\u00e5 enheten.\n 3. Klikk p\u00e5 `...` \u00f8verst til h\u00f8yre.\n 4. Bla til bunnen av siden.\n 5. Deaktiver l\u00e5sen.", + "description": "{name} ({model} p\u00e5 {host}) er l\u00e5st. Du m\u00e5 l\u00e5se opp enheten for \u00e5 godkjenne og fullf\u00f8re konfigurasjonen. Bruksanvisning:\n 1. \u00c5pne Broadlink-appen\n 2. Klikk p\u00e5 enheten\n 3. Klikk p\u00e5 `...` \u00f8verst til h\u00f8yre\n 4. Bla til bunnen av siden\n 5. Deaktiver l\u00e5sen", "title": "L\u00e5s opp enheten" }, "unlock": { "data": { "unlock": "Ja, gj\u00f8r det." }, - "description": "{name} ( {model} p\u00e5 {host} ) er l\u00e5st. Dette kan f\u00f8re til autentiseringsproblemer i Home Assistant. Vil du l\u00e5se opp den?", + "description": "{name} ({model} p\u00e5 {host}) er l\u00e5st. Dette kan f\u00f8re til godkjenningsproblemer i Home Assistant. Vil du l\u00e5se den opp?", "title": "L\u00e5s opp enheten (valgfritt)" }, "user": { diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json index 5dfc087c7b..a2bea167b5 100644 --- a/homeassistant/components/cloud/translations/hu.json +++ b/homeassistant/components/cloud/translations/hu.json @@ -5,6 +5,9 @@ "can_reach_cloud_auth": "Hiteles\u00edt\u00e9si kiszolg\u00e1l\u00f3 el\u00e9r\u00e9se", "google_enabled": "Google enged\u00e9lyezve", "logged_in": "Bejelentkezve", + "relayer_connected": "K\u00f6zvet\u00edt\u0151 csatlakoztatva", + "remote_connected": "T\u00e1voli csatlakoz\u00e1s", + "remote_enabled": "T\u00e1voli hozz\u00e1f\u00e9r\u00e9s enged\u00e9lyezve", "subscription_expiration": "El\u0151fizet\u00e9s lej\u00e1rata" } } diff --git a/homeassistant/components/cloud/translations/no.json b/homeassistant/components/cloud/translations/no.json index 585811f0eb..63779e7fa9 100644 --- a/homeassistant/components/cloud/translations/no.json +++ b/homeassistant/components/cloud/translations/no.json @@ -4,7 +4,7 @@ "alexa_enabled": "Alexa aktivert", "can_reach_cert_server": "N\u00e5 sertifikatserver", "can_reach_cloud": "N\u00e5 Home Assistant Cloud", - "can_reach_cloud_auth": "N\u00e5 autentiseringsserver", + "can_reach_cloud_auth": "N\u00e5 godkjenningsserver", "google_enabled": "Google aktivert", "logged_in": "Logget inn", "relayer_connected": "Relayer tilkoblet", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 5d831d7aaa..d3501c496e 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -15,7 +15,7 @@ "data": { "password": "Passord" }, - "description": "Vennligst fyll inn passordet du har angitt i din konfigurasjon for {name}." + "description": "Vennligst fyll inn passordet du har angitt i din konfigurasjon for {name}" }, "discovery_confirm": { "description": "\u00d8nsker du \u00e5 legge ESPHome noden `{name}` til Home Assistant?", diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json new file mode 100644 index 0000000000..63c887ff28 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "Weboldal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json index 5a4635e1ed..af1ceba2c9 100644 --- a/homeassistant/components/fireservicerota/translations/no.json +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" @@ -15,7 +15,7 @@ "data": { "password": "Passord" }, - "description": "Autentiseringstokener for baceame er ugyldige, logg inn for \u00e5 gjenskape dem." + "description": "Godkjenningstokener ble ugyldige, logg inn for \u00e5 gjenopprette dem" }, "user": { "data": { diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json index 1ea5c9020e..fa34150963 100644 --- a/homeassistant/components/hangouts/translations/no.json +++ b/homeassistant/components/hangouts/translations/no.json @@ -6,13 +6,13 @@ }, "error": { "invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.", - "invalid_2fa_method": "Ugyldig 2FA-metode (Bekreft p\u00e5 telefon).", + "invalid_2fa_method": "Ugyldig totrinnsbekreftelse-metode (Bekreft p\u00e5 telefon)", "invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen." }, "step": { "2fa": { "data": { - "2fa": "2FA Pin" + "2fa": "Totrinnsbekreftelse Pin" }, "description": "", "title": "Totrinnsbekreftelse" diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 4119802eb7..216e8d391b 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -4,6 +4,7 @@ "disk_total": "\u00d6sszes hely", "disk_used": "Felhaszn\u00e1lt hely", "docker_version": "Docker verzi\u00f3", + "healthy": "Eg\u00e9szs\u00e9ges", "host_os": "Gazdag\u00e9p oper\u00e1ci\u00f3s rendszer", "installed_addons": "Telep\u00edtett kieg\u00e9sz\u00edt\u0151k", "supervisor_api": "Adminisztr\u00e1tor API", diff --git a/homeassistant/components/home_connect/translations/no.json b/homeassistant/components/home_connect/translations/no.json index 3d95664d0a..e929ea2f91 100644 --- a/homeassistant/components/home_connect/translations/no.json +++ b/homeassistant/components/home_connect/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json new file mode 100644 index 0000000000..50ccd9f3b6 --- /dev/null +++ b/homeassistant/components/hyperion/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" + }, + "step": { + "auth": { + "data": { + "create_token": "\u00daj token automatikus l\u00e9trehoz\u00e1sa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json index c73f520597..e411982b58 100644 --- a/homeassistant/components/hyperion/translations/no.json +++ b/homeassistant/components/hyperion/translations/no.json @@ -8,7 +8,7 @@ "auth_required_error": "Kan ikke fastsl\u00e5 om autorisasjon er n\u00f8dvendig", "cannot_connect": "Tilkobling mislyktes", "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 2f9571b68f..62e123eb84 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_device": "Ingen av enhetene dine har \"Finn min iPhone\" aktivert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", @@ -16,7 +16,7 @@ "password": "Passord" }, "description": "Ditt tidligere angitte passord for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" }, "trusted_device": { "data": { diff --git a/homeassistant/components/logi_circle/translations/no.json b/homeassistant/components/logi_circle/translations/no.json index 14ffab8711..94aa56bb63 100644 --- a/homeassistant/components/logi_circle/translations/no.json +++ b/homeassistant/components/logi_circle/translations/no.json @@ -4,23 +4,23 @@ "already_configured": "Kontoen er allerede konfigurert", "external_error": "Det oppstod et unntak fra en annen flow.", "external_setup": "Logi Circle er vellykket konfigurert fra en annen flow.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" }, "error": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send", "invalid_auth": "Ugyldig godkjenning" }, "step": { "auth": { - "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Logi Circle kontoen din, kom deretter tilbake og trykk **Send** nedenfor. \n\n [Link]({authorization_url})", + "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Logi Circle kontoen din, kom deretter tilbake og trykk **Send** nedenfor \n\n [Link]({authorization_url})", "title": "Godkjenn med Logi Circle" }, "user": { "data": { "flow_impl": "Tilbyder" }, - "description": "Velg med hvilken godkjenningsleverand\u00f8r du vil godkjenne Logi Circle.", + "description": "Velg med hvilken godkjenningsleverand\u00f8r du vil godkjenne Logi Circle", "title": "Godkjenningsleverand\u00f8r" } } diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json index c44f51b02e..301075e0ad 100644 --- a/homeassistant/components/mobile_app/translations/hu.json +++ b/homeassistant/components/mobile_app/translations/hu.json @@ -8,5 +8,10 @@ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a mobil alkalmaz\u00e1s komponenst?" } } + }, + "device_automation": { + "action_type": { + "notify": "\u00c9rtes\u00edt\u00e9s k\u00fcld\u00e9se" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index e508f2cb29..63ceded565 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -39,12 +39,12 @@ }, "trigger_type": { "button_double_press": "\"{subtype}\" \u53cc\u51fb", - "button_long_press": "\"{subtype}\" \u6301\u7eed\u6309\u4e0b", - "button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u91ca\u653e", + "button_long_press": "\"{subtype}\" \u957f\u6309", + "button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", "button_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb", "button_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb", "button_short_press": "\"{subtype}\" \u6309\u4e0b", - "button_short_release": "\"{subtype}\" \u91ca\u653e", + "button_short_release": "\"{subtype}\" \u677e\u5f00", "button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb" } }, diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 1d88e45b2c..f2fd30f323 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -1,12 +1,16 @@ { "config": { "abort": { - "already_configured": "M\u00e1r konfigur\u00e1lva van" + "already_configured": "M\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" }, "create_entry": { "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )." }, "step": { + "reauth_confirm": { + "title": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index 9db6c2450f..a788c79ff5 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "invalid_auth": "Ugyldig godkjenning", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index daf07ea445..08d8cb9745 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 de Nest ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } }, diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 739d77c826..6693c2e561 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "reauth_successful": "Re-authentication was successful", "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize url." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Nest integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" } } }, diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 2e58ddeedd..7d22dfd96b 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Nesti sidumine peab konto taastuvastama", + "title": "Taastuvasta sidumine" } } }, diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index d9a216305e..47334c4aa6 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -2,13 +2,15 @@ "config": { "abort": { "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", + "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" }, "create_entry": { "default": "Sikeres autentik\u00e1ci\u00f3" }, "error": { "internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l", + "invalid_pin": "\u00c9rv\u00e9nytelen ", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n" }, @@ -26,7 +28,18 @@ }, "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t] ( {url} ). \n\n Az enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az al\u00e1bbi PIN k\u00f3dot.", "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" + }, + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Mozg\u00e1s \u00e9szlelve", + "camera_person": "Szem\u00e9ly \u00e9szlelve", + "camera_sound": "Hang \u00e9szlelve", + "doorbell_chime": "Cseng\u0151 megnyomva" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 4db72851aa..958eaea039 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "reauth_successful": "Riautenticato con successo", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index f1232b5f86..dfaf33b396 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "create_entry": { "default": "Vellykket godkjenning" @@ -29,11 +30,15 @@ "data": { "code": "PIN kode" }, - "description": "For \u00e5 koble din Nest-konto, [bekreft kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", + "description": "For \u00e5 koble din Nest-konto m\u00e5 du [bekrefte kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", "title": "Koble til Nest konto" }, "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Nest-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" } } }, diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 4060808c26..4f2e895256 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Nest", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" } } }, diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 9f18963dcb..387dbe7b26 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index c4b70e9007..c4091388b3 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -3,6 +3,11 @@ "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "reauth": { + "title": "\u00dajrahiteles\u00edt\u00e9s" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 5e0537b663..97bf9fc498 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -11,8 +11,8 @@ "data": { "password": "Passord" }, - "description": "Autentisering mislyktes for OVO Energy. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", - "title": "Reautorisasjon" + "description": "Godkjenning mislyktes for OVO Energy. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/owntracks/translations/no.json b/homeassistant/components/owntracks/translations/no.json index 923fbab244..db992a5630 100644 --- a/homeassistant/components/owntracks/translations/no.json +++ b/homeassistant/components/owntracks/translations/no.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { - "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url}\n - Identificasjon:\n - Brukernavn: ''\n - Enhets ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP\n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autensiering\n - BrukerID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." + "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url}\n - Identifikasjon:\n - Brukernavn: ''\n - Enhets ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP\n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 godkjenning\n - BrukerID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon" }, "step": { "user": { diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index 9729938035..e4c864d9fd 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -3,7 +3,8 @@ "abort": { "addon_info_failed": "Nem siker\u00fclt bet\u00f6lteni az OpenZWave kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3kat.", "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", - "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t." + "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index c5368b675b..ddd6ef4cdb 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -4,7 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json new file mode 100644 index 0000000000..1dcdb7fe5a --- /dev/null +++ b/homeassistant/components/plugwise/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "flow_type": "Kapcsolat t\u00edpusa" + } + }, + "user_gateway": { + "description": "K\u00e9rj\u00fck, adja meg" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index 59dff606f8..d0d0b9114f 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -2,22 +2,22 @@ "config": { "abort": { "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "external_setup": "Punktet er konfigurert fra en annen flyt.", - "no_flows": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "no_flows": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "create_entry": { "default": "Vellykket godkjenning" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send", "no_token": "Ugyldig tilgangstoken" }, "step": { "auth": { - "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Minut-kontoen din, kom tilbake og trykk **Send inn** nedenfor. \n\n [Link]({authorization_url})", + "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Minut-kontoen din, kom tilbake og trykk **Send inn** nedenfor\n\n [Link]({authorization_url})", "title": "Godkjenn Point" }, "user": { diff --git a/homeassistant/components/recollect_waste/translations/ca.json b/homeassistant/components/recollect_waste/translations/ca.json index 395fe5b9da..33d3ddbfaf 100644 --- a/homeassistant/components/recollect_waste/translations/ca.json +++ b/homeassistant/components/recollect_waste/translations/ca.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Utilitza els sobrenoms per als tipus de recollida (quan sigui possible)" + }, + "title": "Configuraci\u00f3 de Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/it.json b/homeassistant/components/recollect_waste/translations/it.json index d52e7be128..5c9a9157ba 100644 --- a/homeassistant/components/recollect_waste/translations/it.json +++ b/homeassistant/components/recollect_waste/translations/it.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Usa nomi descrittivi per i tipi di ritiro (quando possibile)" + }, + "title": "Configura la raccolta dei rifiuti" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/ru.json b/homeassistant/components/recollect_waste/translations/ru.json index 21e926ec9e..c90c1aefee 100644 --- a/homeassistant/components/recollect_waste/translations/ru.json +++ b/homeassistant/components/recollect_waste/translations/ru.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u043d\u044f\u0442\u043d\u044b\u0435 \u0438\u043c\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u0438\u043f\u043e\u0432 \u043f\u043e\u0434\u0431\u043e\u0440\u0449\u0438\u043a\u0430 (\u0435\u0441\u043b\u0438 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e)" + }, + "title": "Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 964b143f1d..4e04ab16ce 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -2,9 +2,23 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "setup_serial": { + "data": { + "device": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" + }, + "title": "Eszk\u00f6z" + }, + "setup_serial_manual_path": { + "title": "El\u00e9r\u00e9si \u00fat" + } } }, "options": { + "error": { + "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d" + }, "step": { "prompt_options": { "data": { diff --git a/homeassistant/components/ring/translations/no.json b/homeassistant/components/ring/translations/no.json index 566568ce37..b1561285eb 100644 --- a/homeassistant/components/ring/translations/no.json +++ b/homeassistant/components/ring/translations/no.json @@ -10,7 +10,7 @@ "step": { "2fa": { "data": { - "2fa": "To-faktorskode" + "2fa": "Totrinnsbekreftelse kode" }, "title": "Totrinnsbekreftelse" }, diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index acfb900218..9067e2c6f5 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -10,8 +10,8 @@ }, "step": { "link": { - "description": "Du m\u00e5 autorisere home assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner Innstillinger og aktiverer HomeAssistant p\u00e5 Utvidelser-fanen.", - "title": "Autoriser HomeAssistant i Roon" + "description": "Du m\u00e5 godkjenne Home Assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner innstillingene og aktiverer Home Assistant p\u00e5 utvidelser-fanen.", + "title": "Autoriser Home Assistant i Roon" }, "user": { "data": { diff --git a/homeassistant/components/sharkiq/translations/no.json b/homeassistant/components/sharkiq/translations/no.json index d04e579654..4454bd940d 100644 --- a/homeassistant/components/sharkiq/translations/no.json +++ b/homeassistant/components/sharkiq/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 401c5a540d..3280224885 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "identifier_exists": "Konto er allerede registrert", @@ -13,14 +13,14 @@ "step": { "mfa": { "description": "Sjekk e-posten din for en lenke fra SimpliSafe. Etter \u00e5 ha bekreftet lenken, g\u00e5 tilbake hit for \u00e5 fullf\u00f8re installasjonen av integrasjonen.", - "title": "SimpliSafe flerfaktorautentisering" + "title": "SimpliSafe flertrinnsbekreftelse" }, "reauth_confirm": { "data": { "password": "Passord" }, - "description": "Adgangstokenet ditt har utl\u00f8pt eller blitt opphevet. Skriv inn passordet ditt for \u00e5 koble kontoen din p\u00e5 nytt.", - "title": "Bekreft integrering p\u00e5 nytt" + "description": "Tilgangstokenet ditt har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble til kontoen din p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json index 5e37836946..f1307e2a16 100644 --- a/homeassistant/components/smappee/translations/no.json +++ b/homeassistant/components/smappee/translations/no.json @@ -3,10 +3,10 @@ "abort": { "already_configured_device": "Enheten er allerede konfigurert", "already_configured_local_device": "Lokal(e) enhet(er) er allerede konfigurert. Fjern de f\u00f8rst f\u00f8r du konfigurerer en skyenhet.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "cannot_connect": "Tilkobling mislyktes", "invalid_mdns": "Ikke-st\u00f8ttet enhet for Smappee-integrasjonen.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" }, "flow_title": "", diff --git a/homeassistant/components/smartthings/translations/no.json b/homeassistant/components/smartthings/translations/no.json index 41bb76282b..ca8c6f81ed 100644 --- a/homeassistant/components/smartthings/translations/no.json +++ b/homeassistant/components/smartthings/translations/no.json @@ -6,9 +6,9 @@ }, "error": { "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", - "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene.", + "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", - "token_unauthorized": "Tokenet er ugyldig eller er ikke lenger godkjent.", + "token_unauthorized": "Tokenet er ugyldig eller er ikke lenger godkjent", "webhook_error": "SmartThings kan ikke validere URL-adressen for webhook. Kontroller at URL-adressen for webhook kan n\u00e5s fra Internett, og pr\u00f8v p\u00e5 nytt." }, "step": { diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index e66bf3b404..3189026992 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "site_not_active": "Az oldal nem akt\u00edv" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index 4b9fe3b564..f9b64dc848 100644 --- a/homeassistant/components/soma/translations/no.json +++ b/homeassistant/components/soma/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", - "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "connection_error": "Kunne ikke koble til SOMA Connect.", "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "result_error": "SOMA Connect svarte med feilstatus." diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json index 2bb48d39f2..57bc6e6843 100644 --- a/homeassistant/components/somfy/translations/no.json +++ b/homeassistant/components/somfy/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 39f14020ae..88fe5330ee 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "error": { @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "Sonarr-integrasjonen m\u00e5 autentiseres p\u00e5 nytt med Sonarr API vert p\u00e5: {host}", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index eee2386a92..8e2ec3d36c 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen.", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "reauth_account_mismatch": "Spotify-kontoen som er autentisert med, samsvarer ikke med den kontoen som trengs re-autentisering." + "reauth_account_mismatch": "Spotify-kontoen som er godkjent samsvarer ikke med kontoen som trenger godkjenning p\u00e5 nytt" }, "create_entry": { "default": "Vellykket godkjenning med Spotify." @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "Spotify-integreringen m\u00e5 godkjennes p\u00e5 nytt med Spotify for konto: {account}", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" } } }, diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json new file mode 100644 index 0000000000..f46e17923a --- /dev/null +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "A fi\u00f3k azonos\u00edt\u00f3ja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json new file mode 100644 index 0000000000..c76efd0e89 --- /dev/null +++ b/homeassistant/components/tasmota/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "config": { + "title": "Tasmota" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 95bd22cbec..649de0f86e 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -2,17 +2,17 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "unknown": "Uventet feil", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "error": { "invalid_auth": "Ugyldig godkjenning" }, "step": { "auth": { - "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})", + "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SEND**. \n\n [TelldusLive-konto]({auth_url})", "title": "Godkjenn mot TelldusLive" }, "user": { diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index e5a72f35e2..a64a64ab74 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Den valgte avtalen er allerede konfigurert.", - "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_agreements": "Denne kontoen har ingen Toon skjermer.", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "step": { "agreement": { diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 5fc7e1050a..2dd7407ad9 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Konfigurasjon oppdatert for profil.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" }, "create_entry": { @@ -26,7 +26,7 @@ }, "reauth": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" } } } diff --git a/homeassistant/components/xbox/translations/no.json b/homeassistant/components/xbox/translations/no.json index 49ccd378c1..4736fc91bf 100644 --- a/homeassistant/components/xbox/translations/no.json +++ b/homeassistant/components/xbox/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index a594ffe354..1dd51cd7e6 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -7,34 +7,78 @@ "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 ZHA \u8bbe\u5907\u3002" }, "step": { + "pick_radio": { + "data": { + "radio_type": "\u65e0\u7ebf\u7535\u7c7b\u578b" + }, + "description": "\u8bf7\u9009\u62e9 Zigbee \u65e0\u7ebf\u7535\u7c7b\u578b", + "title": "\u65e0\u7ebf\u7535\u7c7b\u578b" + }, + "port_config": { + "data": { + "baudrate": "\u6ce2\u7279\u7387", + "flow_control": "\u6570\u636e\u6d41\u63a7\u5236", + "path": "\u4e32\u884c\u8bbe\u5907\u8def\u5f84" + }, + "description": "\u8f93\u5165\u7aef\u53e3\u7684\u7279\u5b9a\u8bbe\u7f6e", + "title": "\u8bbe\u7f6e" + }, "user": { + "data": { + "path": "\u4e32\u884c\u8bbe\u5907\u8def\u5f84" + }, + "description": "\u9009\u62e9 Zigbee \u7684\u4e32\u884c\u7aef\u53e3", "title": "ZHA" } } }, "device_automation": { "action_type": { - "warn": "\u8b66\u544a" + "squawk": "\u54cd\u94c3", + "warn": "\u544a\u8b66" }, "trigger_subtype": { - "both_buttons": "\u4e24\u4e2a\u6309\u94ae", - "button_1": "\u7b2c\u4e00\u4e2a\u6309\u94ae", - "button_2": "\u7b2c\u4e8c\u4e2a\u6309\u94ae", - "button_3": "\u7b2c\u4e09\u4e2a\u6309\u94ae", - "button_4": "\u7b2c\u56db\u4e2a\u6309\u94ae", - "button_5": "\u7b2c\u4e94\u4e2a\u6309\u94ae", - "button_6": "\u7b2c\u516d\u4e2a\u6309\u94ae", + "both_buttons": "\u4e24\u952e\u540c\u65f6", + "button_1": "\u7b2c\u4e00\u952e", + "button_2": "\u7b2c\u4e8c\u952e", + "button_3": "\u7b2c\u4e09\u952e", + "button_4": "\u7b2c\u56db\u952e", + "button_5": "\u7b2c\u4e94\u952e", + "button_6": "\u7b2c\u516d\u952e", + "close": "\u5173\u95ed", "dim_down": "\u8c03\u6697", "dim_up": "\u8c03\u4eae", "left": "\u5de6", "open": "\u5f00\u542f", "right": "\u53f3", - "turn_off": "\u5173\u95ed" + "turn_off": "\u5173\u95ed", + "turn_on": "\u5f00\u542f" }, "trigger_type": { + "device_dropped": "\u8bbe\u5907\u81ea\u7531\u843d\u4f53", + "device_flipped": "\u8bbe\u5907\u7ffb\u8f6c \"{subtype}\"", + "device_knocked": "\u8bbe\u5907\u8f7b\u6572 \"{subtype}\"", "device_offline": "\u8bbe\u5907\u79bb\u7ebf", - "device_tilted": "\u8bbe\u5907\u540d\u79f0", - "remote_button_short_press": "\"{subtype}\" \u6309\u94ae\u5df2\u6309\u4e0b" + "device_rotated": "\u8bbe\u5907\u65cb\u8f6c \"{subtype}\"", + "device_shaken": "\u8bbe\u5907\u6447\u4e00\u6447", + "device_slid": "\u8bbe\u5907\u5e73\u79fb \"{subtype}\"", + "device_tilted": "\u8bbe\u5907\u503e\u659c", + "remote_button_alt_double_press": "\"{subtype}\" \u53cc\u51fb(\u5907\u7528)", + "remote_button_alt_long_press": "\"{subtype}\" \u957f\u6309(\u5907\u7528)", + "remote_button_alt_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00(\u5907\u7528)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb(\u5907\u7528)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb(\u5907\u7528)", + "remote_button_alt_short_press": "\"{subtype}\" \u5355\u51fb(\u5907\u7528)", + "remote_button_alt_short_release": "\"{subtype}\" \u677e\u5f00(\u5907\u7528)", + "remote_button_alt_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb(\u5907\u7528)", + "remote_button_double_press": "\"{subtype}\" \u53cc\u51fb", + "remote_button_long_press": "\"{subtype}\" \u957f\u6309", + "remote_button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", + "remote_button_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb", + "remote_button_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb", + "remote_button_short_press": "\"{subtype}\" \u5355\u51fb", + "remote_button_short_release": "\"{subtype}\" \u677e\u5f00", + "remote_button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb" } } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/no.json b/homeassistant/components/zoneminder/translations/no.json index 50a9b5fedb..b40ea7917f 100644 --- a/homeassistant/components/zoneminder/translations/no.json +++ b/homeassistant/components/zoneminder/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "auth_fail": "Brukernavn eller passord er feil.", + "auth_fail": "Brukernavn eller passord er feil", "cannot_connect": "Tilkobling mislyktes", "connection_error": "Kunne ikke koble til en ZoneMinder-server.", "invalid_auth": "Ugyldig godkjenning" @@ -10,7 +10,7 @@ "default": "ZoneMinder-serveren er lagt til." }, "error": { - "auth_fail": "Brukernavn eller passord er feil.", + "auth_fail": "Brukernavn eller passord er feil", "cannot_connect": "Tilkobling mislyktes", "connection_error": "Kunne ikke koble til en ZoneMinder-server.", "invalid_auth": "Ugyldig godkjenning" From 369cf10eb38241e3df42c9e9dfdb972e7fe32a7f Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Mon, 21 Dec 2020 13:49:53 +0100 Subject: [PATCH 194/302] Bump meteofrance-api to 1.0.1 (#44389) --- homeassistant/components/meteo_france/__init__.py | 4 ++-- homeassistant/components/meteo_france/config_flow.py | 2 +- homeassistant/components/meteo_france/manifest.json | 2 +- homeassistant/components/meteo_france/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/meteo_france/test_config_flow.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 152999b557..3034135f84 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -3,8 +3,8 @@ import asyncio from datetime import timedelta import logging -from meteofrance.client import MeteoFranceClient -from meteofrance.helpers import is_valid_warning_department +from meteofrance_api.client import MeteoFranceClient +from meteofrance_api.helpers import is_valid_warning_department import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index 4593a392ee..f4d7c5dccf 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the Meteo-France integration.""" import logging -from meteofrance.client import MeteoFranceClient +from meteofrance_api.client import MeteoFranceClient import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 97c9b589c4..8de4e76c6f 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": [ - "meteofrance-api==0.1.1" + "meteofrance-api==1.0.1" ], "codeowners": [ "@hacf-fr", diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 00f3c2da2c..8e6b036202 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,7 +1,7 @@ """Support for Meteo-France raining forecast sensor.""" import logging -from meteofrance.helpers import ( +from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, readeable_phenomenoms_dict, ) diff --git a/requirements_all.txt b/requirements_all.txt index dac983b1ac..daf44d50e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -934,7 +934,7 @@ messagebird==1.2.0 meteoalertapi==0.1.6 # homeassistant.components.meteo_france -meteofrance-api==0.1.1 +meteofrance-api==1.0.1 # homeassistant.components.mfi mficlient==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf0074d319..adb344ac47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -462,7 +462,7 @@ mbddns==0.1.2 mcstatus==2.3.0 # homeassistant.components.meteo_france -meteofrance-api==0.1.1 +meteofrance-api==1.0.1 # homeassistant.components.mfi mficlient==0.3.0 diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 3aa70aa48b..6c1d9312f9 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Meteo-France config flow.""" -from meteofrance.model import Place +from meteofrance_api.model import Place import pytest from homeassistant import data_entry_flow From 83794f0629ad0dde2630d95d8ab1cd1b6f2ce010 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 14:25:06 +0100 Subject: [PATCH 195/302] Bump actions/setup-python from v2.2.0 to v2.2.1 (#44420) Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.2.0 to v2.2.1. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.2.0...3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd394234cc..d29ab71cff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -73,7 +73,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -118,7 +118,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -163,7 +163,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -230,7 +230,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -278,7 +278,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -326,7 +326,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -371,7 +371,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -419,7 +419,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -475,7 +475,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -555,7 +555,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.0 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} From 0b1791c29371d80acdd9fcac5597212827fbf95c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Dec 2020 16:35:47 +0100 Subject: [PATCH 196/302] Update denonavr to 0.9.9 (#44411) --- 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 31085292fb..44cbd69bcd 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.8", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.9", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index daf44d50e0..210e440a15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -481,7 +481,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.8 +denonavr==0.9.9 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adb344ac47..e8e173e1b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.8 +denonavr==0.9.9 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 From cb82f5159e871202614fc54c7093f6166683381d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 21 Dec 2020 15:53:47 +0000 Subject: [PATCH 197/302] Reduce IPP errors when printer is offline (#44413) --- homeassistant/components/ipp/__init__.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 9f522b086f..7a18da03dd 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -48,22 +48,24 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" - # Create IPP instance for this entry - coordinator = IPPDataUpdateCoordinator( - hass, - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - base_path=entry.data[CONF_BASE_PATH], - tls=entry.data[CONF_SSL], - verify_ssl=entry.data[CONF_VERIFY_SSL], - ) + coordinator = hass.data[DOMAIN].get(entry.entry_id) + if not coordinator: + # Create IPP instance for this entry + coordinator = IPPDataUpdateCoordinator( + hass, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + base_path=entry.data[CONF_BASE_PATH], + tls=entry.data[CONF_SSL], + verify_ssl=entry.data[CONF_VERIFY_SSL], + ) + hass.data[DOMAIN][entry.entry_id] = coordinator + await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) From bf253819dda72d7a87ce5f8880759e59fd1158cf Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Mon, 21 Dec 2020 17:11:53 +0100 Subject: [PATCH 198/302] Add additional debug launch methods in launch.json (#44419) --- .vscode/launch.json | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6976e26ebb..3d967b25c1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,41 @@ "type": "python", "request": "launch", "module": "homeassistant", - "args": ["--debug", "-c", "config"] + "args": [ + "--debug", + "-c", + "config" + ] + }, + { + // Debug by attaching to local Home Asistant server using Remote Python Debugger. + // See https://www.home-assistant.io/integrations/debugpy/ + "name": "Home Assistant: Attach Local", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ], + }, + { + // Debug by attaching to remote Home Asistant server using Remote Python Debugger. + // See https://www.home-assistant.io/integrations/debugpy/ + "name": "Home Assistant: Attach Remote", + "type": "python", + "request": "attach", + "port": 5678, + "host": "homeassistant.local", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/usr/src/homeassistant" + } + ], } ] -} +} \ No newline at end of file From f89c682ea6bc3cc68e1fa5737c7871c0243a3075 Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Mon, 21 Dec 2020 12:50:31 -0600 Subject: [PATCH 199/302] Cleanup and optimization for Zerproc (#44430) * Cleanup and optimization for Zerproc * Remove unnecessary extra method * Add debug log for exceptions on disconnect --- homeassistant/components/zerproc/light.py | 17 +++++++++-------- homeassistant/components/zerproc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zerproc/test_light.py | 10 ++++++++++ 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index fc1f0c9a70..89f60faf84 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -110,17 +110,18 @@ class ZerprocLight(LightEntity): """Run when entity about to be added to hass.""" self.async_on_remove( self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.on_hass_shutdown + EVENT_HOMEASSISTANT_STOP, self.async_will_remove_from_hass ) ) - async def async_will_remove_from_hass(self) -> None: + async def async_will_remove_from_hass(self, *args) -> None: """Run when entity will be removed from hass.""" - await self._light.disconnect() - - async def on_hass_shutdown(self, event): - """Execute when Home Assistant is shutting down.""" - await self._light.disconnect() + try: + await self._light.disconnect() + except pyzerproc.ZerprocException: + _LOGGER.debug( + "Exception disconnected from %s", self.entity_id, exc_info=True + ) @property def name(self): @@ -192,7 +193,7 @@ class ZerprocLight(LightEntity): async def async_update(self): """Fetch new state data for this light.""" try: - if not await self._light.is_connected(): + if not self._available: await self._light.connect() state = await self._light.get_state() except pyzerproc.ZerprocException: diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index d5a61fd18a..54b70d7867 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": [ - "pyzerproc==0.4.3" + "pyzerproc==0.4.7" ], "codeowners": [ "@emlove" diff --git a/requirements_all.txt b/requirements_all.txt index 210e440a15..1ba5d3d200 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1904,7 +1904,7 @@ pyxeoma==1.4.1 pyzbar==0.1.7 # homeassistant.components.zerproc -pyzerproc==0.4.3 +pyzerproc==0.4.7 # homeassistant.components.qnap qnapstats==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8e173e1b6..b6613a840f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ pywemo==0.5.3 pywilight==0.0.65 # homeassistant.components.zerproc -pyzerproc==0.4.3 +pyzerproc==0.4.7 # homeassistant.components.rachio rachiopy==1.0.3 diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 92d8d2638b..77fdfb7d48 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -173,6 +173,16 @@ async def test_remove_entry(hass, mock_light, mock_entry): assert mock_disconnect.called +async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry): + """Assert that disconnect exceptions are caught.""" + with patch.object( + mock_light, "disconnect", side_effect=pyzerproc.ZerprocException("Mock error") + ) as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + async def test_light_turn_on(hass, mock_light): """Test ZerprocLight turn_on.""" utcnow = dt_util.utcnow() From 1ca99c4fa4b2fcb3d1580fe36931a83d82518f20 Mon Sep 17 00:00:00 2001 From: treylok Date: Mon, 21 Dec 2020 13:03:26 -0600 Subject: [PATCH 200/302] Add ecobee humidity attributes (#44366) * Update humidity attributes. * Update climate.py * Raise ValueError * Update conditions for humidity controls to show Humidity controls only show when the humidifier mode is in "manual" mode. --- homeassistant/components/ecobee/climate.py | 39 +++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 94396bbf88..6bb7dc1a87 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -65,6 +65,11 @@ AWAY_MODE = "awayMode" PRESET_HOME = "home" PRESET_SLEEP = "sleep" +DEFAULT_MIN_HUMIDITY = 15 +DEFAULT_MAX_HUMIDITY = 50 +HUMIDIFIER_MANUAL_MODE = "manual" + + # Order matters, because for reverse mapping we don't want to map HEAT to AUX ECOBEE_HVAC_TO_HASS = collections.OrderedDict( [ @@ -162,7 +167,6 @@ SUPPORT_FLAGS = ( | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE - | SUPPORT_TARGET_HUMIDITY ) @@ -332,6 +336,8 @@ class Thermostat(ClimateEntity): @property def supported_features(self): """Return the list of supported features.""" + if self.has_humidifier_control: + return SUPPORT_FLAGS | SUPPORT_TARGET_HUMIDITY return SUPPORT_FLAGS @property @@ -391,6 +397,31 @@ class Thermostat(ClimateEntity): return self.thermostat["runtime"]["desiredCool"] / 10.0 return None + @property + def has_humidifier_control(self): + """Return true if humidifier connected to thermostat and set to manual/on mode.""" + return ( + self.thermostat["settings"]["hasHumidifier"] + and self.thermostat["settings"]["humidifierMode"] == HUMIDIFIER_MANUAL_MODE + ) + + @property + def target_humidity(self) -> Optional[int]: + """Return the desired humidity set point.""" + if self.has_humidifier_control: + return self.thermostat["runtime"]["desiredHumidity"] + return None + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return DEFAULT_MIN_HUMIDITY + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return DEFAULT_MAX_HUMIDITY + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -653,7 +684,13 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity): """Set the humidity level.""" + if humidity not in range(0, 101): + raise ValueError( + f"Invalid set_humidity value (must be in range 0-100): {humidity}" + ) + self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) + self.update_without_throttle = True def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" From 56b3e0b52e8dab00dcbb38eade72da386ebbd645 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Dec 2020 21:55:06 +0100 Subject: [PATCH 201/302] Support area on entities for google assistant (#44300) * Support area on entities * Don't let registry area override config --- .../components/google_assistant/helpers.py | 47 +++++++++++-------- .../google_assistant/test_smart_home.py | 14 ++++-- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 48664bff39..0063342293 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Context, HomeAssistant, State, callback +from homeassistant.helpers.area_registry import AreaEntry from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store @@ -39,6 +40,29 @@ SYNC_DELAY = 15 _LOGGER = logging.getLogger(__name__) +async def _get_area(hass, entity_id) -> Optional[AreaEntry]: + """Calculate the area for a entity_id.""" + dev_reg, ent_reg, area_reg = await gather( + hass.helpers.device_registry.async_get_registry(), + hass.helpers.entity_registry.async_get_registry(), + hass.helpers.area_registry.async_get_registry(), + ) + + entity_entry = ent_reg.async_get(entity_id) + if not entity_entry: + return None + + if entity_entry.area_id: + area_id = entity_entry.area_id + else: + device_entry = dev_reg.devices.get(entity_entry.device_id) + if not (device_entry and device_entry.area_id): + return None + area_id = device_entry.area_id + + return area_reg.areas.get(area_id) + + class AbstractConfig(ABC): """Hold the configuration for Google Assistant.""" @@ -450,25 +474,10 @@ class GoogleEntity: room = entity_config.get(CONF_ROOM_HINT) if room: device["roomHint"] = room - return device - - dev_reg, ent_reg, area_reg = await gather( - self.hass.helpers.device_registry.async_get_registry(), - self.hass.helpers.entity_registry.async_get_registry(), - self.hass.helpers.area_registry.async_get_registry(), - ) - - entity_entry = ent_reg.async_get(state.entity_id) - if not (entity_entry and entity_entry.device_id): - return device - - device_entry = dev_reg.devices.get(entity_entry.device_id) - if not (device_entry and device_entry.area_id): - return device - - area_entry = area_reg.areas.get(device_entry.area_id) - if area_entry and area_entry.name: - device["roomHint"] = area_entry.name + else: + area = await _get_area(self.hass, state.entity_id) + if area and area.name: + device["roomHint"] = area.name return device diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 27e62fafc7..ebe34c2aa9 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -152,7 +152,8 @@ async def test_sync_message(hass): # pylint: disable=redefined-outer-name -async def test_sync_in_area(hass, registries): +@pytest.mark.parametrize("area_on_device", [True, False]) +async def test_sync_in_area(area_on_device, hass, registries): """Test a sync message where room hint comes from area.""" area = registries.area.async_create("Living Room") @@ -160,10 +161,17 @@ async def test_sync_in_area(hass, registries): config_entry_id="1234", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - registries.device.async_update_device(device.id, area_id=area.id) + registries.device.async_update_device( + device.id, area_id=area.id if area_on_device else None + ) entity = registries.entity.async_get_or_create( - "light", "test", "1235", suggested_object_id="demo_light", device_id=device.id + "light", + "test", + "1235", + suggested_object_id="demo_light", + device_id=device.id, + area_id=area.id if not area_on_device else None, ) light = DemoLight( From 2ee2f85574885b684fe638add25428026f2313e1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 22 Dec 2020 00:06:12 +0000 Subject: [PATCH 202/302] [ci skip] Translation update --- .../components/nest/translations/zh-Hant.json | 5 +++++ .../recollect_waste/translations/zh-Hant.json | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index b097aec56a..a271ca666f 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Nest \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } }, diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index b40bc6d197..75615c1cce 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u91dd\u5c0d\u9078\u53d6\u985e\u578b\u4f7f\u7528\u53cb\u5584\u540d\u7a31\uff08\u5047\u5982\u9069\u7528\uff09" + }, + "title": "\u8a2d\u5b9a Recollect Waste" + } + } } } \ No newline at end of file From d4453908d586b548618597dbab90b54956f7624e Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 22 Dec 2020 13:32:56 +0200 Subject: [PATCH 203/302] Fix Volumio pause with missing track type (#44447) --- homeassistant/components/volumio/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 89fb17affc..69790e7173 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -204,7 +204,7 @@ class Volumio(MediaPlayerEntity): async def async_media_pause(self): """Send media_pause command to media player.""" - if self._state["trackType"] == "webradio": + if self._state.get("trackType") == "webradio": await self._volumio.stop() else: await self._volumio.pause() From 67ed730c08bac283777f9bc60e925b50b853ab98 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 22 Dec 2020 12:39:50 +0100 Subject: [PATCH 204/302] KNX BinarySensor takes float values for `reset_after` (#44446) --- homeassistant/components/knx/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c17667cbed..b1d791e328 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -112,7 +112,7 @@ class BinarySensorSchema: ), vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Optional(CONF_INVERT): cv.boolean, - vol.Optional(CONF_RESET_AFTER): cv.positive_int, + vol.Optional(CONF_RESET_AFTER): cv.positive_float, } ), ) From ffbef0bcd13bb88e5af672a81f1dd250fbe6fae0 Mon Sep 17 00:00:00 2001 From: PhiBo Date: Tue, 22 Dec 2020 13:28:37 +0100 Subject: [PATCH 205/302] Fix KNX issue if 0 kelvin is reported by device (#44392) Co-authored-by: Matthias Alphart Co-authored-by: Franck Nijhof --- homeassistant/components/knx/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index d9f0f9c0d3..50d067bf29 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -86,7 +86,8 @@ class KNXLight(KnxEntity, LightEntity): """Return the color temperature in mireds.""" if self._device.supports_color_temperature: kelvin = self._device.current_color_temperature - if kelvin is not None: + # Avoid division by zero if actuator reported 0 Kelvin (e.g., uninitialized DALI-Gateway) + if kelvin is not None and kelvin > 0: return color_util.color_temperature_kelvin_to_mired(kelvin) if self._device.supports_tunable_white: relative_ct = self._device.current_tunable_white From 1027361f278788ecbffe8a13b419ffd5ab41b1c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Dec 2020 13:33:37 +0100 Subject: [PATCH 206/302] Bump codecov/codecov-action from v1.1.0 to v1.1.1 (#44442) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.1.0 to v1.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.1.0...1fc7722ded4708880a5aea49f2bfafb9336f0c8d) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d29ab71cff..a33cd59f22 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -785,4 +785,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.1.0 + uses: codecov/codecov-action@v1.1.1 From 9c5f608ffd5dc55a007403025e3a7c5894e7e2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 22 Dec 2020 14:49:42 +0200 Subject: [PATCH 207/302] Remove Travis CI config (#44443) https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing https://docs.travis-ci.com/user/migrate/open-source-repository-migration/#frequently-asked-questions --- .travis.yml | 42 ------------------------------------------ docs/source/conf.py | 1 - 2 files changed, 43 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 218bb1132a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -dist: focal -addons: - apt: - packages: - - ffmpeg - - libudev-dev - - libavformat-dev - - libavcodec-dev - - libavdevice-dev - - libavutil-dev - - libswscale-dev - - libswresample-dev - - libavfilter-dev - -python: - - "3.7.1" - - "3.8" - -env: - - TOX_ARGS="-- --test-group-count 4 --test-group 1" - - TOX_ARGS="-- --test-group-count 4 --test-group 2" - - TOX_ARGS="-- --test-group-count 4 --test-group 3" - - TOX_ARGS="-- --test-group-count 4 --test-group 4" - -jobs: - fast_finish: true - include: - - python: "3.7.1" - env: TOXENV=lint - - python: "3.7.1" - # PYLINT_ARGS=--jobs=0 disabled for now: https://github.com/PyCQA/pylint/issues/3584 - env: TOXENV=pylint TRAVIS_WAIT=30 - - python: "3.7.1" - env: TOXENV=typing - -cache: - pip: true - directories: - - $HOME/.cache/pre-commit -install: pip install -U tox tox-travis -language: python -script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox -vv --develop ${TOX_ARGS-} diff --git a/docs/source/conf.py b/docs/source/conf.py index 242a90088b..ab09df87ae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -207,7 +207,6 @@ html_theme_options = { "github_repo": PROJECT_GITHUB_REPOSITORY, "github_type": "star", "github_banner": True, - "travis_button": True, "touch_icon": "logo-apple.png", # 'fixed_sidebar': True, # Re-enable when we have more content } From 24ccdb55bb0a9e281e58acb9304763230db25d9f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 22 Dec 2020 12:42:37 -0800 Subject: [PATCH 208/302] Move Legacy Works With Nest integration to subdirectory (#44368) * Move Legacy Works With Nest integration to subdirectory Motivation is to streamline the actively developed integration e.g. make code coverage easier to reason about and simplify __init__.py --- .coveragerc | 5 +- homeassistant/components/nest/__init__.py | 410 +---------------- .../components/nest/binary_sensor.py | 173 +------- homeassistant/components/nest/camera.py | 2 +- homeassistant/components/nest/climate.py | 2 +- .../components/nest/legacy/__init__.py | 416 ++++++++++++++++++ .../components/nest/legacy/binary_sensor.py | 167 +++++++ .../{camera_legacy.py => legacy/camera.py} | 9 +- .../{climate_legacy.py => legacy/climate.py} | 7 +- homeassistant/components/nest/legacy/const.py | 6 + .../nest/{ => legacy}/local_auth.py | 10 +- .../{sensor_legacy.py => legacy/sensor.py} | 4 +- homeassistant/components/nest/sensor.py | 2 +- tests/components/nest/test_local_auth.py | 3 +- 14 files changed, 625 insertions(+), 591 deletions(-) create mode 100644 homeassistant/components/nest/legacy/__init__.py create mode 100644 homeassistant/components/nest/legacy/binary_sensor.py rename homeassistant/components/nest/{camera_legacy.py => legacy/camera.py} (95%) rename homeassistant/components/nest/{climate_legacy.py => legacy/climate.py} (98%) create mode 100644 homeassistant/components/nest/legacy/const.py rename homeassistant/components/nest/{ => legacy}/local_auth.py (85%) rename homeassistant/components/nest/{sensor_legacy.py => legacy/sensor.py} (98%) diff --git a/.coveragerc b/.coveragerc index a8459a2cd7..f637b64149 100644 --- a/.coveragerc +++ b/.coveragerc @@ -579,12 +579,9 @@ omit = homeassistant/components/nest/api.py homeassistant/components/nest/binary_sensor.py homeassistant/components/nest/camera.py - homeassistant/components/nest/camera_legacy.py homeassistant/components/nest/climate.py - homeassistant/components/nest/climate_legacy.py - homeassistant/components/nest/local_auth.py + homeassistant/components/nest/legacy/* homeassistant/components/nest/sensor.py - homeassistant/components/nest/sensor_legacy.py homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py homeassistant/components/netatmo/camera.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 151b1dac00..e9bffa2706 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,45 +1,32 @@ """Support for Nest devices.""" import asyncio -from datetime import datetime, timedelta import logging -import threading from google_nest_sdm.event import AsyncEventCallback, EventMessage from google_nest_sdm.exceptions import AuthException, GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber -from nest import Nest -from nest.nest import APIError, AuthorizationError import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_FILENAME, CONF_MONITORED_CONDITIONS, CONF_SENSORS, CONF_STRUCTURE, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, config_validation as cv, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, - dispatcher_send, -) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import api, config_flow, local_auth +from . import api, config_flow from .const import ( API_URL, DATA_SDM, @@ -50,34 +37,15 @@ from .const import ( SIGNAL_NEST_UPDATE, ) from .events import EVENT_NAME_MAP, NEST_EVENT +from .legacy import async_setup_legacy, async_setup_legacy_entry _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" - - -# Configuration for the legacy nest API -SERVICE_CANCEL_ETA = "cancel_eta" -SERVICE_SET_ETA = "set_eta" - -DATA_NEST = "nest" DATA_NEST_CONFIG = "nest_config" -NEST_CONFIG_FILE = "nest.conf" - -ATTR_ETA = "eta" -ATTR_ETA_WINDOW = "eta_window" -ATTR_STRUCTURE = "structure" -ATTR_TRIP_ID = "trip_id" - -AWAY_MODE_AWAY = "away" -AWAY_MODE_HOME = "home" - -ATTR_AWAY_MODE = "away_mode" -SERVICE_SET_AWAY_MODE = "set_away_mode" - SENSOR_SCHEMA = vol.Schema( {vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)} ) @@ -104,31 +72,6 @@ CONFIG_SCHEMA = vol.Schema( # Platforms for SDM API PLATFORMS = ["sensor", "camera", "climate"] -# Services for the legacy API - -SET_AWAY_MODE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - -SET_ETA_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ETA): cv.time_period, - vol.Optional(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_ETA_WINDOW): cv.time_period, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - -CANCEL_ETA_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - async def async_setup(hass: HomeAssistant, config: dict): """Set up Nest components with dispatch between old/new flows.""" @@ -283,348 +226,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(DATA_SUBSCRIBER) return unload_ok - - -def nest_update_event_broker(hass, nest): - """ - Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. - - Used for the legacy nest API. - - Runs in its own thread. - """ - _LOGGER.debug("Listening for nest.update_event") - - while hass.is_running: - nest.update_event.wait() - - if not hass.is_running: - break - - nest.update_event.clear() - _LOGGER.debug("Dispatching nest data update") - dispatcher_send(hass, SIGNAL_NEST_UPDATE) - - _LOGGER.debug("Stop listening for nest.update_event") - - -async def async_setup_legacy(hass, config): - """Set up Nest components using the legacy nest API.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) - - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - access_token_cache_file = hass.config.path(filename) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"nest_conf_path": access_token_cache_file}, - ) - ) - - # Store config to be used during entry setup - hass.data[DATA_NEST_CONFIG] = conf - - return True - - -async def async_setup_legacy_entry(hass, entry): - """Set up Nest from legacy config entry.""" - - nest = Nest(access_token=entry.data["tokens"]["access_token"]) - - _LOGGER.debug("proceeding with setup") - conf = hass.data.get(DATA_NEST_CONFIG, {}) - hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest) - if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): - return False - - for component in "climate", "camera", "sensor", "binary_sensor": - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - - def validate_structures(target_structures): - all_structures = [structure.name for structure in nest.structures] - for target in target_structures: - if target not in all_structures: - _LOGGER.info("Invalid structure: %s", target) - - def set_away_mode(service): - """Set the away mode for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - _LOGGER.info( - "Setting away mode for: %s to: %s", - structure.name, - service.data[ATTR_AWAY_MODE], - ) - structure.away = service.data[ATTR_AWAY_MODE] - - def set_eta(service): - """Set away mode to away and include ETA for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - if structure.thermostats: - _LOGGER.info( - "Setting away mode for: %s to: %s", - structure.name, - AWAY_MODE_AWAY, - ) - structure.away = AWAY_MODE_AWAY - - now = datetime.utcnow() - trip_id = service.data.get( - ATTR_TRIP_ID, f"trip_{int(now.timestamp())}" - ) - eta_begin = now + service.data[ATTR_ETA] - eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1)) - eta_end = eta_begin + eta_window - _LOGGER.info( - "Setting ETA for trip: %s, " - "ETA window starts at: %s and ends at: %s", - trip_id, - eta_begin, - eta_end, - ) - structure.set_eta(trip_id, eta_begin, eta_end) - else: - _LOGGER.info( - "No thermostats found in structure: %s, unable to set ETA", - structure.name, - ) - - def cancel_eta(service): - """Cancel ETA for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - if structure.thermostats: - trip_id = service.data[ATTR_TRIP_ID] - _LOGGER.info("Cancelling ETA for trip: %s", trip_id) - structure.cancel_eta(trip_id) - else: - _LOGGER.info( - "No thermostats found in structure: %s, " - "unable to cancel ETA", - structure.name, - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA - ) - - @callback - def start_up(event): - """Start Nest update event listener.""" - threading.Thread( - name="Nest update listener", - target=nest_update_event_broker, - args=(hass, nest), - ).start() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) - - @callback - def shut_down(event): - """Stop Nest update event listener.""" - nest.update_event.set() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) - - _LOGGER.debug("async_setup_nest is done") - - return True - - -class NestLegacyDevice: - """Structure Nest functions for hass for legacy API.""" - - def __init__(self, hass, conf, nest): - """Init Nest Devices.""" - self.hass = hass - self.nest = nest - self.local_structure = conf.get(CONF_STRUCTURE) - - def initialize(self): - """Initialize Nest.""" - try: - # Do not optimize next statement, it is here for initialize - # persistence Nest API connection. - structure_names = [s.name for s in self.nest.structures] - if self.local_structure is None: - self.local_structure = structure_names - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - return False - return True - - def structures(self): - """Generate a list of structures.""" - try: - for structure in self.nest.structures: - if structure.name not in self.local_structure: - _LOGGER.debug( - "Ignoring structure %s, not in %s", - structure.name, - self.local_structure, - ) - continue - yield structure - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - - def thermostats(self): - """Generate a list of thermostats.""" - return self._devices("thermostats") - - def smoke_co_alarms(self): - """Generate a list of smoke co alarms.""" - return self._devices("smoke_co_alarms") - - def cameras(self): - """Generate a list of cameras.""" - return self._devices("cameras") - - def _devices(self, device_type): - """Generate a list of Nest devices.""" - try: - for structure in self.nest.structures: - if structure.name not in self.local_structure: - _LOGGER.debug( - "Ignoring structure %s, not in %s", - structure.name, - self.local_structure, - ) - continue - - for device in getattr(structure, device_type, []): - try: - # Do not optimize next statement, - # it is here for verify Nest API permission. - device.name_long - except KeyError: - _LOGGER.warning( - "Cannot retrieve device name for [%s]" - ", please check your Nest developer " - "account permission settings", - device.serial, - ) - continue - yield (structure, device) - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - - -class NestSensorDevice(Entity): - """Representation of a Nest sensor.""" - - def __init__(self, structure, device, variable): - """Initialize the sensor.""" - self.structure = structure - self.variable = variable - - if device is not None: - # device specific - self.device = device - self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}" - else: - # structure only - self.device = structure - self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}" - - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - - @property - def unique_id(self): - """Return unique id based on device serial and variable.""" - return f"{self.device.serial}-{self.variable}" - - @property - def device_info(self): - """Return information about the device.""" - if not hasattr(self.device, "name_long"): - name = self.structure.name - model = "Structure" - else: - name = self.device.name_long - if self.device.is_thermostat: - model = "Thermostat" - elif self.device.is_camera: - model = "Camera" - elif self.device.is_smoke_co_alarm: - model = "Nest Protect" - else: - model = None - - return { - "identifiers": {(DOMAIN, self.device.serial)}, - "name": name, - "manufacturer": "Nest Labs", - "model": model, - } - - def update(self): - """Do not use NestSensorDevice directly.""" - raise NotImplementedError - - async def async_added_to_hass(self): - """Register update signal handler.""" - - async def async_update_state(): - """Update sensor state.""" - await self.async_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) - ) diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 56d4ac31b7..dc58dd2856 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -1,166 +1,15 @@ -"""Support for Nest Thermostat binary sensors.""" -from itertools import chain -import logging +"""Support for Nest binary sensors that dispatches between API versions.""" -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_SOUND, - BinarySensorEntity, -) -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice - -_LOGGER = logging.getLogger(__name__) - -BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY} - -CLIMATE_BINARY_TYPES = { - "fan": None, - "is_using_emergency_heat": "heat", - "is_locked": None, - "has_leaf": None, -} - -CAMERA_BINARY_TYPES = { - "motion_detected": DEVICE_CLASS_MOTION, - "sound_detected": DEVICE_CLASS_SOUND, - "person_detected": DEVICE_CLASS_OCCUPANCY, -} - -STRUCTURE_BINARY_TYPES = {"away": None} - -STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}} - -_BINARY_TYPES_DEPRECATED = [ - "hvac_ac_state", - "hvac_aux_heater_state", - "hvac_heater_state", - "hvac_heat_x2_state", - "hvac_heat_x3_state", - "hvac_alt_heat_state", - "hvac_alt_heat_x2_state", - "hvac_emer_heat_state", -] - -_VALID_BINARY_SENSOR_TYPES = { - **BINARY_TYPES, - **CLIMATE_BINARY_TYPES, - **CAMERA_BINARY_TYPES, - **STRUCTURE_BINARY_TYPES, -} +from .const import DATA_SDM +from .legacy.sensor import async_setup_legacy_entry -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Nest binary sensors. - - No longer used. - """ - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up a Nest binary sensor based on a config entry.""" - nest = hass.data[DATA_NEST] - - discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) - - # Add all available binary sensors if no Nest binary sensor config is set - if discovery_info == {}: - conditions = _VALID_BINARY_SENSOR_TYPES - else: - conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) - - for variable in conditions: - if variable in _BINARY_TYPES_DEPRECATED: - wstr = ( - f"{variable} is no a longer supported " - "monitored_conditions. See " - "https://www.home-assistant.io/integrations/binary_sensor.nest/ " - "for valid options." - ) - _LOGGER.error(wstr) - - def get_binary_sensors(): - """Get the Nest binary sensors.""" - sensors = [] - for structure in nest.structures(): - sensors += [ - NestBinarySensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_BINARY_TYPES - ] - device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) - for structure, device in device_chain: - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in BINARY_TYPES - ] - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CLIMATE_BINARY_TYPES and device.is_thermostat - ] - - if device.is_camera: - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CAMERA_BINARY_TYPES - ] - for activity_zone in device.activity_zones: - sensors += [ - NestActivityZoneSensor(structure, device, activity_zone) - ] - - return sensors - - async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True) - - -class NestBinarySensor(NestSensorDevice, BinarySensorEntity): - """Represents a Nest binary sensor.""" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return _VALID_BINARY_SENSOR_TYPES.get(self.variable) - - def update(self): - """Retrieve latest state.""" - value = getattr(self.device, self.variable) - if self.variable in STRUCTURE_BINARY_TYPES: - self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value)) - else: - self._state = bool(value) - - -class NestActivityZoneSensor(NestBinarySensor): - """Represents a Nest binary sensor for activity in a zone.""" - - def __init__(self, structure, device, zone): - """Initialize the sensor.""" - super().__init__(structure, device, "") - self.zone = zone - self._name = f"{self._name} {self.zone.name} activity" - - @property - def unique_id(self): - """Return unique id based on camera serial and zone id.""" - return f"{self.device.serial}-{self.zone.zone_id}" - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return DEVICE_CLASS_MOTION - - def update(self): - """Retrieve latest state.""" - self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the binary sensors.""" + assert DATA_SDM not in entry.data + await async_setup_legacy_entry(hass, entry, async_add_entities) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index dfa365a36c..f0e0b8e05f 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -3,9 +3,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from .camera_legacy import async_setup_legacy_entry from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM +from .legacy.camera import async_setup_legacy_entry async def async_setup_entry( diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 6e457da039..a74a50b0f3 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -3,9 +3,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from .climate_legacy import async_setup_legacy_entry from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM +from .legacy.climate import async_setup_legacy_entry async def async_setup_entry( diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py new file mode 100644 index 0000000000..218b01fd71 --- /dev/null +++ b/homeassistant/components/nest/legacy/__init__.py @@ -0,0 +1,416 @@ +"""Support for Nest devices.""" + +from datetime import datetime, timedelta +import logging +import threading + +from nest import Nest +from nest.nest import APIError, AuthorizationError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_FILENAME, + CONF_STRUCTURE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity + +from . import local_auth +from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +# Configuration for the legacy nest API +SERVICE_CANCEL_ETA = "cancel_eta" +SERVICE_SET_ETA = "set_eta" + +NEST_CONFIG_FILE = "nest.conf" + +ATTR_ETA = "eta" +ATTR_ETA_WINDOW = "eta_window" +ATTR_STRUCTURE = "structure" +ATTR_TRIP_ID = "trip_id" + +AWAY_MODE_AWAY = "away" +AWAY_MODE_HOME = "home" + +ATTR_AWAY_MODE = "away_mode" +SERVICE_SET_AWAY_MODE = "set_away_mode" + +# Services for the legacy API + +SET_AWAY_MODE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), + } +) + +SET_ETA_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ETA): cv.time_period, + vol.Optional(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_ETA_WINDOW): cv.time_period, + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), + } +) + +CANCEL_ETA_SCHEMA = vol.Schema( + { + vol.Required(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def nest_update_event_broker(hass, nest): + """ + Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. + + Used for the legacy nest API. + + Runs in its own thread. + """ + _LOGGER.debug("Listening for nest.update_event") + + while hass.is_running: + nest.update_event.wait() + + if not hass.is_running: + break + + nest.update_event.clear() + _LOGGER.debug("Dispatching nest data update") + dispatcher_send(hass, SIGNAL_NEST_UPDATE) + + _LOGGER.debug("Stop listening for nest.update_event") + + +async def async_setup_legacy(hass, config): + """Set up Nest components using the legacy nest API.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) + + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + access_token_cache_file = hass.config.path(filename) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"nest_conf_path": access_token_cache_file}, + ) + ) + + # Store config to be used during entry setup + hass.data[DATA_NEST_CONFIG] = conf + + return True + + +async def async_setup_legacy_entry(hass, entry): + """Set up Nest from legacy config entry.""" + + nest = Nest(access_token=entry.data["tokens"]["access_token"]) + + _LOGGER.debug("proceeding with setup") + conf = hass.data.get(DATA_NEST_CONFIG, {}) + hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest) + if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): + return False + + for component in "climate", "camera", "sensor", "binary_sensor": + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + def validate_structures(target_structures): + all_structures = [structure.name for structure in nest.structures] + for target in target_structures: + if target not in all_structures: + _LOGGER.info("Invalid structure: %s", target) + + def set_away_mode(service): + """Set the away mode for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + _LOGGER.info( + "Setting away mode for: %s to: %s", + structure.name, + service.data[ATTR_AWAY_MODE], + ) + structure.away = service.data[ATTR_AWAY_MODE] + + def set_eta(service): + """Set away mode to away and include ETA for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + if structure.thermostats: + _LOGGER.info( + "Setting away mode for: %s to: %s", + structure.name, + AWAY_MODE_AWAY, + ) + structure.away = AWAY_MODE_AWAY + + now = datetime.utcnow() + trip_id = service.data.get( + ATTR_TRIP_ID, f"trip_{int(now.timestamp())}" + ) + eta_begin = now + service.data[ATTR_ETA] + eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1)) + eta_end = eta_begin + eta_window + _LOGGER.info( + "Setting ETA for trip: %s, " + "ETA window starts at: %s and ends at: %s", + trip_id, + eta_begin, + eta_end, + ) + structure.set_eta(trip_id, eta_begin, eta_end) + else: + _LOGGER.info( + "No thermostats found in structure: %s, unable to set ETA", + structure.name, + ) + + def cancel_eta(service): + """Cancel ETA for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + if structure.thermostats: + trip_id = service.data[ATTR_TRIP_ID] + _LOGGER.info("Cancelling ETA for trip: %s", trip_id) + structure.cancel_eta(trip_id) + else: + _LOGGER.info( + "No thermostats found in structure: %s, " + "unable to cancel ETA", + structure.name, + ) + + hass.services.async_register( + DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA + ) + + hass.services.async_register( + DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA + ) + + hass.services.async_register( + DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA + ) + + @callback + def start_up(event): + """Start Nest update event listener.""" + threading.Thread( + name="Nest update listener", + target=nest_update_event_broker, + args=(hass, nest), + ).start() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + + @callback + def shut_down(event): + """Stop Nest update event listener.""" + nest.update_event.set() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + + _LOGGER.debug("async_setup_nest is done") + + return True + + +class NestLegacyDevice: + """Structure Nest functions for hass for legacy API.""" + + def __init__(self, hass, conf, nest): + """Init Nest Devices.""" + self.hass = hass + self.nest = nest + self.local_structure = conf.get(CONF_STRUCTURE) + + def initialize(self): + """Initialize Nest.""" + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, OSError) as err: + _LOGGER.error("Connection error while access Nest web service: %s", err) + return False + return True + + def structures(self): + """Generate a list of structures.""" + try: + for structure in self.nest.structures: + if structure.name not in self.local_structure: + _LOGGER.debug( + "Ignoring structure %s, not in %s", + structure.name, + self.local_structure, + ) + continue + yield structure + + except (AuthorizationError, APIError, OSError) as err: + _LOGGER.error("Connection error while access Nest web service: %s", err) + + def thermostats(self): + """Generate a list of thermostats.""" + return self._devices("thermostats") + + def smoke_co_alarms(self): + """Generate a list of smoke co alarms.""" + return self._devices("smoke_co_alarms") + + def cameras(self): + """Generate a list of cameras.""" + return self._devices("cameras") + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + try: + for structure in self.nest.structures: + if structure.name not in self.local_structure: + _LOGGER.debug( + "Ignoring structure %s, not in %s", + structure.name, + self.local_structure, + ) + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning( + "Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings", + device.serial, + ) + continue + yield (structure, device) + + except (AuthorizationError, APIError, OSError) as err: + _LOGGER.error("Connection error while access Nest web service: %s", err) + + +class NestSensorDevice(Entity): + """Representation of a Nest sensor.""" + + def __init__(self, structure, device, variable): + """Initialize the sensor.""" + self.structure = structure + self.variable = variable + + if device is not None: + # device specific + self.device = device + self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}" + else: + # structure only + self.device = structure + self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}" + + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + @property + def unique_id(self): + """Return unique id based on device serial and variable.""" + return f"{self.device.serial}-{self.variable}" + + @property + def device_info(self): + """Return information about the device.""" + if not hasattr(self.device, "name_long"): + name = self.structure.name + model = "Structure" + else: + name = self.device.name_long + if self.device.is_thermostat: + model = "Thermostat" + elif self.device.is_camera: + model = "Camera" + elif self.device.is_smoke_co_alarm: + model = "Nest Protect" + else: + model = None + + return { + "identifiers": {(DOMAIN, self.device.serial)}, + "name": name, + "manufacturer": "Nest Labs", + "model": model, + } + + def update(self): + """Do not use NestSensorDevice directly.""" + raise NotImplementedError + + async def async_added_to_hass(self): + """Register update signal handler.""" + + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + ) diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py new file mode 100644 index 0000000000..4470bd1467 --- /dev/null +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -0,0 +1,167 @@ +"""Support for Nest Thermostat binary sensors.""" +from itertools import chain +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) +from homeassistant.const import CONF_BINARY_SENSORS, CONF_MONITORED_CONDITIONS + +from . import NestSensorDevice +from .const import DATA_NEST, DATA_NEST_CONFIG + +_LOGGER = logging.getLogger(__name__) + +BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY} + +CLIMATE_BINARY_TYPES = { + "fan": None, + "is_using_emergency_heat": "heat", + "is_locked": None, + "has_leaf": None, +} + +CAMERA_BINARY_TYPES = { + "motion_detected": DEVICE_CLASS_MOTION, + "sound_detected": DEVICE_CLASS_SOUND, + "person_detected": DEVICE_CLASS_OCCUPANCY, +} + +STRUCTURE_BINARY_TYPES = {"away": None} + +STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}} + +_BINARY_TYPES_DEPRECATED = [ + "hvac_ac_state", + "hvac_aux_heater_state", + "hvac_heater_state", + "hvac_heat_x2_state", + "hvac_heat_x3_state", + "hvac_alt_heat_state", + "hvac_alt_heat_x2_state", + "hvac_emer_heat_state", +] + +_VALID_BINARY_SENSOR_TYPES = { + **BINARY_TYPES, + **CLIMATE_BINARY_TYPES, + **CAMERA_BINARY_TYPES, + **STRUCTURE_BINARY_TYPES, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nest binary sensors. + + No longer used. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Nest binary sensor based on a config entry.""" + nest = hass.data[DATA_NEST] + + discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) + + # Add all available binary sensors if no Nest binary sensor config is set + if discovery_info == {}: + conditions = _VALID_BINARY_SENSOR_TYPES + else: + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) + + for variable in conditions: + if variable in _BINARY_TYPES_DEPRECATED: + wstr = ( + f"{variable} is no a longer supported " + "monitored_conditions. See " + "https://www.home-assistant.io/integrations/binary_sensor.nest/ " + "for valid options." + ) + _LOGGER.error(wstr) + + def get_binary_sensors(): + """Get the Nest binary sensors.""" + sensors = [] + for structure in nest.structures(): + sensors += [ + NestBinarySensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_BINARY_TYPES + ] + device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) + for structure, device in device_chain: + sensors += [ + NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in BINARY_TYPES + ] + sensors += [ + NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CLIMATE_BINARY_TYPES and device.is_thermostat + ] + + if device.is_camera: + sensors += [ + NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CAMERA_BINARY_TYPES + ] + for activity_zone in device.activity_zones: + sensors += [ + NestActivityZoneSensor(structure, device, activity_zone) + ] + + return sensors + + async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True) + + +class NestBinarySensor(NestSensorDevice, BinarySensorEntity): + """Represents a Nest binary sensor.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return _VALID_BINARY_SENSOR_TYPES.get(self.variable) + + def update(self): + """Retrieve latest state.""" + value = getattr(self.device, self.variable) + if self.variable in STRUCTURE_BINARY_TYPES: + self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value)) + else: + self._state = bool(value) + + +class NestActivityZoneSensor(NestBinarySensor): + """Represents a Nest binary sensor for activity in a zone.""" + + def __init__(self, structure, device, zone): + """Initialize the sensor.""" + super().__init__(structure, device, "") + self.zone = zone + self._name = f"{self._name} {self.zone.name} activity" + + @property + def unique_id(self): + """Return unique id based on camera serial and zone id.""" + return f"{self.device.serial}-{self.zone.zone_id}" + + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return DEVICE_CLASS_MOTION + + def update(self): + """Retrieve latest state.""" + self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) diff --git a/homeassistant/components/nest/camera_legacy.py b/homeassistant/components/nest/legacy/camera.py similarity index 95% rename from homeassistant/components/nest/camera_legacy.py rename to homeassistant/components/nest/legacy/camera.py index 48d9cb0078..cc9be9d758 100644 --- a/homeassistant/components/nest/camera_legacy.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -4,10 +4,11 @@ import logging import requests -from homeassistant.components import nest from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera from homeassistant.util.dt import utcnow +from .const import DATA_NEST, DOMAIN + _LOGGER = logging.getLogger(__name__) NEST_BRAND = "Nest" @@ -24,9 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_legacy_entry(hass, entry, async_add_entities): """Set up a Nest sensor based on a config entry.""" - camera_devices = await hass.async_add_executor_job( - hass.data[nest.DATA_NEST].cameras - ) + camera_devices = await hass.async_add_executor_job(hass.data[DATA_NEST].cameras) cameras = [NestCamera(structure, device) for structure, device in camera_devices] async_add_entities(cameras, True) @@ -63,7 +62,7 @@ class NestCamera(Camera): def device_info(self): """Return information about the device.""" return { - "identifiers": {(nest.DOMAIN, self.device.device_id)}, + "identifiers": {(DOMAIN, self.device.device_id)}, "name": self.device.name_long, "manufacturer": "Nest Labs", "model": "Camera", diff --git a/homeassistant/components/nest/climate_legacy.py b/homeassistant/components/nest/legacy/climate.py similarity index 98% rename from homeassistant/components/nest/climate_legacy.py rename to homeassistant/components/nest/legacy/climate.py index ee28a0905c..cd0d66acba 100644 --- a/homeassistant/components/nest/climate_legacy.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -1,4 +1,4 @@ -"""Support for Nest thermostats.""" +"""Legacy Works with Nest climate implementation.""" import logging from nest.nest import APIError @@ -33,8 +33,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_NEST, DOMAIN as NEST_DOMAIN -from .const import SIGNAL_NEST_UPDATE +from .const import DATA_NEST, DOMAIN, SIGNAL_NEST_UPDATE _LOGGER = logging.getLogger(__name__) @@ -170,7 +169,7 @@ class NestThermostat(ClimateEntity): def device_info(self): """Return information about the device.""" return { - "identifiers": {(NEST_DOMAIN, self.device.device_id)}, + "identifiers": {(DOMAIN, self.device.device_id)}, "name": self.device.name_long, "manufacturer": "Nest Labs", "model": "Thermostat", diff --git a/homeassistant/components/nest/legacy/const.py b/homeassistant/components/nest/legacy/const.py new file mode 100644 index 0000000000..664606b9ed --- /dev/null +++ b/homeassistant/components/nest/legacy/const.py @@ -0,0 +1,6 @@ +"""Constants used by the legacy Nest component.""" + +DOMAIN = "nest" +DATA_NEST = "nest" +DATA_NEST_CONFIG = "nest_config" +SIGNAL_NEST_UPDATE = "nest_update" diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/legacy/local_auth.py similarity index 85% rename from homeassistant/components/nest/local_auth.py rename to homeassistant/components/nest/legacy/local_auth.py index 8be2693325..f5fb286df7 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/legacy/local_auth.py @@ -7,14 +7,14 @@ from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth from homeassistant.const import HTTP_UNAUTHORIZED from homeassistant.core import callback -from . import config_flow +from ..config_flow import CodeInvalid, NestAuthError, register_flow_implementation from .const import DOMAIN @callback def initialize(hass, client_id, client_secret): """Initialize a local auth provider.""" - config_flow.register_flow_implementation( + register_flow_implementation( hass, DOMAIN, "configuration.yaml", @@ -44,7 +44,7 @@ async def resolve_auth_code(hass, client_id, client_secret, code): return await result except AuthorizationError as err: if err.response.status_code == HTTP_UNAUTHORIZED: - raise config_flow.CodeInvalid() - raise config_flow.NestAuthError( + raise CodeInvalid() from err + raise NestAuthError( f"Unknown error: {err} ({err.response.status_code})" - ) + ) from err diff --git a/homeassistant/components/nest/sensor_legacy.py b/homeassistant/components/nest/legacy/sensor.py similarity index 98% rename from homeassistant/components/nest/sensor_legacy.py rename to homeassistant/components/nest/legacy/sensor.py index 2df668513e..34f525ca7a 100644 --- a/homeassistant/components/nest/sensor_legacy.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -3,6 +3,7 @@ import logging from homeassistant.const import ( CONF_MONITORED_CONDITIONS, + CONF_SENSORS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -11,7 +12,8 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) -from . import CONF_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice +from . import NestSensorDevice +from .const import DATA_NEST, DATA_NEST_CONFIG SENSOR_TYPES = ["humidity", "operation_mode", "hvac_state"] diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 6245c5d83d..0dcc89e226 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SDM -from .sensor_legacy import async_setup_legacy_entry +from .legacy.sensor import async_setup_legacy_entry from .sensor_sdm import async_setup_sdm_entry diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py index 491b9bd9e0..ecc37bbe24 100644 --- a/tests/components/nest/test_local_auth.py +++ b/tests/components/nest/test_local_auth.py @@ -4,7 +4,8 @@ from urllib.parse import parse_qsl import pytest import requests_mock as rmock -from homeassistant.components.nest import config_flow, const, local_auth +from homeassistant.components.nest import config_flow, const +from homeassistant.components.nest.legacy import local_auth @pytest.fixture From 139fb518d6f523c5759e56b334f92051cf2473f1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 23 Dec 2020 00:03:22 +0000 Subject: [PATCH 209/302] [ci skip] Translation update --- .../accuweather/translations/de.json | 1 + .../components/airly/translations/de.json | 5 +++ .../components/apple_tv/translations/de.json | 20 ++++++++---- .../cover/translations/zh-Hans.json | 9 +++++- .../fireservicerota/translations/de.json | 17 ++++++++-- .../components/hyperion/translations/de.json | 31 +++++++++++++++++++ .../components/hyperion/translations/es.json | 3 +- .../mobile_app/translations/de.json | 2 +- .../motion_blinds/translations/de.json | 18 +++++++++++ .../components/neato/translations/de.json | 2 +- .../components/neato/translations/es.json | 15 +++++++-- .../components/nest/translations/de.json | 8 ++++- .../components/nest/translations/es.json | 5 +++ .../components/ozw/translations/de.json | 8 ++++- .../components/point/translations/de.json | 3 +- .../recollect_waste/translations/es.json | 9 ++++++ .../components/solaredge/translations/de.json | 7 ++++- .../components/spotify/translations/de.json | 5 +++ .../tellduslive/translations/de.json | 3 +- .../components/toon/translations/de.json | 3 +- 20 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/motion_blinds/translations/de.json diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 5a13056e68..fe0319764a 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -25,6 +25,7 @@ }, "system_health": { "info": { + "can_reach_server": "AccuWeather Server erreichen", "remaining_requests": "Verbleibende erlaubte Anfragen" } } diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index 19768cad7d..743a68a010 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -18,5 +18,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Airly Server erreichen" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index 4c72601942..464bad99d5 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -3,9 +3,9 @@ "abort": { "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "backoff": "Ger\u00e4t akzeptiert derzeit keine Kopplungsanfragen (Sie haben m\u00f6glicherweise zu oft einen ung\u00fcltigen PIN-Code eingegeben), versuchen Sie es sp\u00e4ter erneut.", + "backoff": "Das Ger\u00e4t akzeptiert derzeit keine Kopplungsanfragen (M\u00f6glicherweise wurde zu oft ein ung\u00fcltiger PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", - "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuchen Sie, es erneut hinzuzuf\u00fcgen.", + "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuche, es erneut hinzuzuf\u00fcgen.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "unknown": "Unerwarteter Fehler" }, @@ -13,21 +13,28 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.", "unknown": "Unerwarteter Fehler" }, "flow_title": "Apple TV: {name}", "step": { + "confirm": { + "description": "Es wird der Apple TV mit dem Namen \" {name} \" zu Home Assistant hinzugef\u00fcgt. \n\n ** Um den Vorgang abzuschlie\u00dfen, m\u00fcssen m\u00f6glicherweise mehrere PIN-Codes eingegeben werden. ** \n\n Bitte beachte, dass der Apple TV mit dieser Integration * nicht * ausgeschalten werden kann. Nur der Media Player in Home Assistant wird ausgeschaltet!", + "title": "Best\u00e4tige das Hinzuf\u00fcgen vom Apple TV" + }, "pair_no_pin": { + "description": "F\u00fcr den Dienst `{protocol}` ist eine Kopplung erforderlich. Bitte gebe die PIN {pin} am Apple TV ein, um fortzufahren.", "title": "Kopplung" }, "pair_with_pin": { "data": { "pin": "PIN-Code" }, + "description": "F\u00fcr das Protokoll `{protocol}` ist eine Kopplung erforderlich. Bitte gebe den auf dem Bildschirm angezeigten PIN-Code ein. F\u00fchrende Nullen m\u00fcssen weggelassen werden, d.h. gebe 123 ein, wenn der angezeigte Code 0123 lautet.", "title": "Kopplung" }, "reconfigure": { - "description": "Dieses Apple TV hat Verbindungsprobleme und muss neu konfiguriert werden.", + "description": "Dieser Apple TV hat Verbindungsprobleme und muss neu konfiguriert werden.", "title": "Ger\u00e4teneukonfiguration" }, "service_problem": { @@ -38,7 +45,8 @@ "data": { "device_input": "Ger\u00e4t" }, - "title": "Einrichten eines neuen Apple TV" + "description": "Gebe zun\u00e4chst den Ger\u00e4tenamen (z. B. K\u00fcche oder Schlafzimmer) oder die IP-Adresse des Apple TV ein, der hinzugef\u00fcgt werden soll. Wenn Ger\u00e4te automatisch im Netzwerk gefunden wurden, werden sie unten angezeigt. \n\nWenn das Ger\u00e4t nicht sichtbar ist oder Probleme auftreten, gebe die IP-Adresse des Ger\u00e4ts an. \n\n{devices}", + "title": "Neuen Apple TV einrichten" } } }, @@ -46,9 +54,9 @@ "step": { "init": { "data": { - "start_off": "Schalten Sie das Ger\u00e4t nicht ein, wenn Sie Home Assistant starten" + "start_off": "Schalte das Ger\u00e4t nicht ein, wenn Home Assistant startet" }, - "description": "Konfigurieren Sie allgemeine Ger\u00e4teeinstellungen" + "description": "Konfiguriere die allgemeinen Ger\u00e4teeinstellungen" } } }, diff --git a/homeassistant/components/cover/translations/zh-Hans.json b/homeassistant/components/cover/translations/zh-Hans.json index 7c5675dad3..04b25ad7cb 100644 --- a/homeassistant/components/cover/translations/zh-Hans.json +++ b/homeassistant/components/cover/translations/zh-Hans.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "close": "\u5173\u95ed {entity_name}", + "open": "\u6253\u5f00 {entity_name}", + "set_position": "\u8bbe\u7f6e {entity_name} \u7684\u4f4d\u7f6e", "stop": "\u505c\u6b62 {entity_name}" }, "condition_type": { @@ -12,7 +15,11 @@ "is_tilt_position": "{entity_name} \u5f53\u524d\u503e\u659c\u4f4d\u7f6e\u4e3a" }, "trigger_type": { - "closed": "{entity_name}\u5df2\u5173\u95ed" + "closed": "{entity_name} \u5df2\u5173\u95ed", + "closing": "{entity_name} \u6b63\u5728\u5173\u95ed", + "opened": "{entity_name} \u5df2\u6253\u5f00", + "opening": "{entity_name} \u6b63\u5728\u6253\u5f00", + "position": "{entity_name} \u7684\u4f4d\u7f6e\u53d8\u5316" } }, "state": { diff --git a/homeassistant/components/fireservicerota/translations/de.json b/homeassistant/components/fireservicerota/translations/de.json index 35636c0fd9..737fbc5ff5 100644 --- a/homeassistant/components/fireservicerota/translations/de.json +++ b/homeassistant/components/fireservicerota/translations/de.json @@ -1,14 +1,27 @@ { "config": { + "abort": { + "already_configured": "Account wurde schon konfiguriert", + "reauth_successful": "Neuauthentifizierung erfolgreich" + }, + "create_entry": { + "default": "Authentifizierung erfolgreich" + }, + "error": { + "invalid_auth": "Authentifizienung ung\u00fcltig" + }, "step": { "reauth": { "data": { "password": "Passwort" - } + }, + "description": "Authentifizierungs-Tokens sind ung\u00fcltig, melde dich an, um sie neu zu erstellen." }, "user": { "data": { - "password": "Passwort" + "password": "Passwort", + "url": "Webseite", + "username": "Nutzername" } } } diff --git a/homeassistant/components/hyperion/translations/de.json b/homeassistant/components/hyperion/translations/de.json index 0d6bcb6747..95c0f1734c 100644 --- a/homeassistant/components/hyperion/translations/de.json +++ b/homeassistant/components/hyperion/translations/de.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "Der Dienst ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "auth_new_token_not_granted_error": "Neu erstellter Token wurde auf der Hyperion-Benutzeroberfl\u00e4che nicht genehmigt", + "auth_new_token_not_work_error": "Authentifizierung mit neu erstelltem Token fehlgeschlagen", + "auth_required_error": "Es konnte nicht festgestellt werden, ob eine Autorisierung erforderlich ist", "cannot_connect": "Verbindung fehlgeschlagen", + "no_id": "Die Hyperion Ambilight-Instanz hat ihre ID nicht gemeldet", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { @@ -11,6 +15,24 @@ "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token" }, "step": { + "auth": { + "data": { + "create_token": "Automatisch neuen Authentifizierungs-Token erstellen", + "token": "Oder stelle einen bereits vorhandenen Token bereit" + }, + "description": "Konfiguriere die Autorisierung f\u00fcr den Hyperion-Ambilight-Server" + }, + "confirm": { + "description": "Soll dieses Hyperion Ambilight zu Home Assistant hinzugef\u00fcgt werden? \n\n ** Host: ** {host}\n ** Port: ** {port}\n ** ID **: {id}", + "title": "Best\u00e4tige das Hinzuf\u00fcgen des Hyperion-Ambilight-Dienstes" + }, + "create_token": { + "description": "W\u00e4hle **Submit**, um einen neuen Authentifizierungs-Token anzufordern. Du wirst zur Hyperion-Benutzeroberfl\u00e4che weitergeleitet, um die Anforderung zu best\u00e4tigen. Bitte \u00fcberpr\u00fcfe, ob die angezeigte ID \"{auth_id}\" lautet.", + "title": "Automatisch neuen Authentifizierungs-Token erstellen" + }, + "create_token_external": { + "title": "Neuen Token in Hyperion UI akzeptieren" + }, "user": { "data": { "host": "Host", @@ -18,5 +40,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion-Priorit\u00e4t f\u00fcr Farben und Effekte" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index bb1ef3e2c0..db3aa75462 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", "cannot_connect": "No se pudo conectar", - "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n" + "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/mobile_app/translations/de.json b/homeassistant/components/mobile_app/translations/de.json index 2289539915..493ceb4dfd 100644 --- a/homeassistant/components/mobile_app/translations/de.json +++ b/homeassistant/components/mobile_app/translations/de.json @@ -11,7 +11,7 @@ }, "device_automation": { "action_type": { - "notify": "Eine Benachrichtigung senden" + "notify": "Sende eine Benachrichtigung" } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json new file mode 100644 index 0000000000..dd1acc230f --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "connection_error": "Verbindung fehlgeschlagen" + }, + "flow_title": "Jalousien", + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "host": "IP-Adresse" + }, + "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Jalousien" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 426dbbe399..94fcd3c4cb 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachte die Dokumentation.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index abe1d21c90..b88a9d0cfa 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -2,7 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { "default": "Ver [documentaci\u00f3n Neato]({docs_url})." @@ -12,6 +16,12 @@ "unknown": "Error inesperado" }, "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "title": "\u00bfQuieres iniciar la configuraci\u00f3n?" + }, "user": { "data": { "password": "Contrase\u00f1a", @@ -22,5 +32,6 @@ "title": "Informaci\u00f3n de la cuenta de Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 400fd8eb93..2bc328ff8f 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL" + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", + "reauth_successful": "Neuathentifizierung erfolgreich", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "error": { "internal_error": "Ein interner Fehler ist aufgetreten", @@ -24,6 +26,10 @@ }, "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.", "title": "Nest-Konto verkn\u00fcpfen" + }, + "reauth_confirm": { + "description": "Die Nest-Integration muss das Konto neu authentifizieren", + "title": "Integration neu authentifizieren" } } }, diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index da5d717cb3..4c0b8b2617 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Nest necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } }, diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json index 815b87f2ec..70eaaaf18d 100644 --- a/homeassistant/components/ozw/translations/de.json +++ b/homeassistant/components/ozw/translations/de.json @@ -5,9 +5,15 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet" }, + "progress": { + "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." + }, "step": { "hassio_confirm": { - "title": "Einrichten der OpenZWave Integration mit dem OpenZWave Add-On" + "title": "Richte die OpenZWave Integration mit dem OpenZWave Add-On ein" + }, + "install_addon": { + "title": "Die Installation des OpenZWave-Add-On wurde gestartet" } } } diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index 1e224e5ac5..8ee83eab72 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", - "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/)." + "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "create_entry": { "default": "Erfolgreich authentifiziert" diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json index 5771c9da9a..2fdeb991bf 100644 --- a/homeassistant/components/recollect_waste/translations/es.json +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/de.json b/homeassistant/components/solaredge/translations/de.json index d3fe05bce1..ec9b5681e7 100644 --- a/homeassistant/components/solaredge/translations/de.json +++ b/homeassistant/components/solaredge/translations/de.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", "site_exists": "Diese site_id ist bereits konfiguriert" }, "error": { - "site_exists": "Diese site_id ist bereits konfiguriert" + "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", + "could_not_connect": "Es konnte keine Verbindung zur Solaredge-API hergestellt werden", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "site_exists": "Diese site_id ist bereits konfiguriert", + "site_not_active": "Die Seite ist nicht aktiv" }, "step": { "user": { diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index b6a10d7cde..bfd393bbbb 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -12,5 +12,10 @@ "title": "Authentifizierungsmethode ausw\u00e4hlen" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify-API-Endpunkt erreichbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index 768f114ac4..a1f6f595a0 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Dienst ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "unknown": "Unbekannter Fehler ist aufgetreten" + "unknown": "Unbekannter Fehler ist aufgetreten", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "step": { "auth": { diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index 4f4dd8a095..d9060a719d 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_agreements": "Dieses Konto hat keine Toon-Anzeigen." + "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" } } } \ No newline at end of file From 769513d6a8e588ce8a180bd8f9e5d7f9d4ce5de0 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 23 Dec 2020 12:46:04 -0800 Subject: [PATCH 210/302] Bump hyperion-py to 0.6.1 (#44490) --- homeassistant/components/hyperion/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 7d6c6222d2..5f5e8ea622 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -8,7 +8,7 @@ "name": "Hyperion", "quality_scale": "platinum", "requirements": [ - "hyperion-py==0.6.0" + "hyperion-py==0.6.1" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 1ba5d3d200..1e3c34e191 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ huawei-lte-api==1.4.12 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.6.0 +hyperion-py==0.6.1 # homeassistant.components.bh1750 # homeassistant.components.bme280 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6613a840f..dcdd3df923 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.12 # homeassistant.components.hyperion -hyperion-py==0.6.0 +hyperion-py==0.6.1 # homeassistant.components.iaqualink iaqualink==0.3.4 From 82f9de31b1e42465b4461e934a354e992b34f2b5 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 24 Dec 2020 00:15:11 +0100 Subject: [PATCH 211/302] Motion Blinds upgrade to local push (#44391) * Motion Blinds upgrade to local push --- .../components/motion_blinds/__init__.py | 62 +++++++++++++++---- .../components/motion_blinds/config_flow.py | 7 +-- .../components/motion_blinds/const.py | 6 +- .../components/motion_blinds/cover.py | 21 +++++++ .../components/motion_blinds/gateway.py | 9 ++- .../components/motion_blinds/manifest.json | 4 +- .../components/motion_blinds/sensor.py | 41 ++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../motion_blinds/test_config_flow.py | 6 +- 10 files changed, 136 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 72929e1ecb..95acd6a0c1 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,22 +1,29 @@ """The motion_blinds component.""" -from asyncio import TimeoutError as AsyncioTimeoutError +import asyncio from datetime import timedelta import logging from socket import timeout +from motionblinds import MotionMulticast + from homeassistant import config_entries, core -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER +from .const import ( + DOMAIN, + KEY_COORDINATOR, + KEY_GATEWAY, + KEY_MULTICAST_LISTENER, + MANUFACTURER, + MOTION_PLATFORMS, +) from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) -MOTION_PLATFORMS = ["cover", "sensor"] - async def async_setup(hass: core.HomeAssistant, config: dict): """Set up the Motion Blinds component.""" @@ -31,8 +38,23 @@ async def async_setup_entry( host = entry.data[CONF_HOST] key = entry.data[CONF_API_KEY] + # Create multicast Listener + if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: + multicast = MotionMulticast() + hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast + # start listening for local pushes (only once) + await hass.async_add_executor_job(multicast.Start_listen) + + # register stop callback to shutdown listening for local pushes + def stop_motion_multicast(event): + """Stop multicast thread.""" + _LOGGER.debug("Shutting down Motion Listener") + multicast.Stop_listen() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_motion_multicast) + # Connect to motion gateway - connect_gateway_class = ConnectMotionGateway(hass) + connect_gateway_class = ConnectMotionGateway(hass, multicast) if not await connect_gateway_class.async_connect_gateway(host, key): raise ConfigEntryNotReady motion_gateway = connect_gateway_class.gateway_device @@ -41,14 +63,19 @@ async def async_setup_entry( """Call all updates using one async_add_executor_job.""" motion_gateway.Update() for blind in motion_gateway.device_list.values(): - blind.Update() + try: + blind.Update() + except timeout: + # let the error be logged and handled by the motionblinds library + pass async def async_update_data(): """Fetch data from the gateway and blinds.""" try: await hass.async_add_executor_job(update_gateway) - except timeout as socket_timeout: - raise AsyncioTimeoutError from socket_timeout + except timeout: + # let the error be logged and handled by the motionblinds library + pass coordinator = DataUpdateCoordinator( hass, @@ -57,7 +84,7 @@ async def async_setup_entry( name=entry.title, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=10), + update_interval=timedelta(seconds=600), ) # Fetch initial data so we have data when entities subscribe @@ -91,11 +118,22 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "cover" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in MOTION_PLATFORMS + ] + ) ) if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) + if len(hass.data[DOMAIN]) == 1: + # No motion gateways left, stop Motion multicast + _LOGGER.debug("Shutting down Motion Listener") + multicast = hass.data[DOMAIN].pop(KEY_MULTICAST_LISTENER) + await hass.async_add_executor_job(multicast.Stop_listen) + return unload_ok diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index fbee7d1b43..497f11760f 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -7,12 +7,11 @@ from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST # pylint: disable=unused-import -from .const import DOMAIN +from .const import DEFAULT_GATEWAY_NAME, DOMAIN from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) -DEFAULT_GATEWAY_NAME = "Motion Gateway" CONFIG_SCHEMA = vol.Schema( { @@ -26,7 +25,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Motion Blinds config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Motion Blinds flow.""" @@ -48,7 +47,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect(self, user_input=None): """Connect to the Motion Gateway.""" - connect_gateway_class = ConnectMotionGateway(self.hass) + connect_gateway_class = ConnectMotionGateway(self.hass, None) if not await connect_gateway_class.async_connect_gateway(self.host, self.key): return self.async_abort(reason="connection_error") motion_gateway = connect_gateway_class.gateway_device diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index c80c8f881c..e5a84041d3 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -1,6 +1,10 @@ """Constants for the Motion Blinds component.""" DOMAIN = "motion_blinds" -MANUFACTURER = "Motion, Coulisse B.V." +MANUFACTURER = "Motion Blinds, Coulisse B.V." +DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway" + +MOTION_PLATFORMS = ["cover", "sensor"] KEY_GATEWAY = "gateway" KEY_COORDINATOR = "coordinator" +KEY_MULTICAST_LISTENER = "multicast_listener" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 4273be3f43..c1895aa566 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -15,6 +15,7 @@ from homeassistant.components.cover import ( DEVICE_CLASS_SHUTTER, CoverEntity, ) +from homeassistant.core import callback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER @@ -125,6 +126,11 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Return the name of the blind.""" return f"{self._blind.blind_type}-{self._blind.mac[12:]}" + @property + def available(self): + """Return True if entity is available.""" + return self._blind.available + @property def current_cover_position(self): """ @@ -146,6 +152,21 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Return if the cover is closed or not.""" return self._blind.position == 100 + @callback + def _push_callback(self): + """Update entity state when a push has been received.""" + self.schedule_update_ha_state(force_refresh=False) + + async def async_added_to_hass(self): + """Subscribe to multicast pushes.""" + self._blind.Register_callback(self.unique_id, self._push_callback) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._blind.Remove_callback(self.unique_id) + await super().async_will_remove_from_hass() + def open_cover(self, **kwargs): """Open the cover.""" self._blind.Open() diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index e7e665d65f..14dd36ce5b 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -10,9 +10,10 @@ _LOGGER = logging.getLogger(__name__) class ConnectMotionGateway: """Class to async connect to a Motion Gateway.""" - def __init__(self, hass): + def __init__(self, hass, multicast): """Initialize the entity.""" self._hass = hass + self._multicast = multicast self._gateway_device = None @property @@ -24,11 +25,15 @@ class ConnectMotionGateway: """Update all information of the gateway.""" self.gateway_device.GetDeviceList() self.gateway_device.Update() + for blind in self.gateway_device.device_list.values(): + blind.Update_from_cache() async def async_connect_gateway(self, host, key): """Connect to the Motion Gateway.""" _LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3]) - self._gateway_device = MotionGateway(ip=host, key=key) + self._gateway_device = MotionGateway( + ip=host, key=key, multicast=self._multicast + ) try: # update device info and get the connected sub devices await self._hass.async_add_executor_job(self.update_gateway) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 84cf711ac9..ce781266a6 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,6 +3,6 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.1.6"], + "requirements": ["motionblinds==0.4.7"], "codeowners": ["@starkillerOG"] -} \ No newline at end of file +} diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 81d555806e..0c31ca070c 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -71,6 +72,11 @@ class MotionBatterySensor(CoordinatorEntity, Entity): """Return the name of the blind battery sensor.""" return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}" + @property + def available(self): + """Return True if entity is available.""" + return self._blind.available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -91,6 +97,21 @@ class MotionBatterySensor(CoordinatorEntity, Entity): """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} + @callback + def push_callback(self): + """Update entity state when a push has been received.""" + self.schedule_update_ha_state(force_refresh=False) + + async def async_added_to_hass(self): + """Subscribe to multicast pushes.""" + self._blind.Register_callback(self.unique_id, self.push_callback) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._blind.Remove_callback(self.unique_id) + await super().async_will_remove_from_hass() + class MotionTDBUBatterySensor(MotionBatterySensor): """ @@ -160,6 +181,11 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity): return "Motion gateway signal strength" return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}" + @property + def available(self): + """Return True if entity is available.""" + return self._device.available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -179,3 +205,18 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity): def state(self): """Return the state of the sensor.""" return self._device.RSSI + + @callback + def push_callback(self): + """Update entity state when a push has been received.""" + self.schedule_update_ha_state(force_refresh=False) + + async def async_added_to_hass(self): + """Subscribe to multicast pushes.""" + self._device.Register_callback(self.unique_id, self.push_callback) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._device.Remove_callback(self.unique_id) + await super().async_will_remove_from_hass() diff --git a/requirements_all.txt b/requirements_all.txt index 1e3c34e191..68e2c5187d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -952,7 +952,7 @@ minio==4.0.9 mitemp_bt==0.0.3 # homeassistant.components.motion_blinds -motionblinds==0.1.6 +motionblinds==0.4.7 # homeassistant.components.tts mutagen==1.45.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcdd3df923..7f9c7134fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -474,7 +474,7 @@ millheater==0.4.0 minio==4.0.9 # homeassistant.components.motion_blinds -motionblinds==0.1.6 +motionblinds==0.4.7 # homeassistant.components.tts mutagen==1.45.1 diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index faa3e7115b..4514beda8c 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -8,10 +8,11 @@ from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_N from homeassistant.components.motion_blinds.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST -from tests.async_mock import patch +from tests.async_mock import Mock, patch TEST_HOST = "1.2.3.4" TEST_API_KEY = "12ab345c-d67e-8f" +TEST_DEVICE_LIST = {"mac": Mock()} @pytest.fixture(name="motion_blinds_connect", autouse=True) @@ -23,6 +24,9 @@ def motion_blinds_connect_fixture(): ), patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.Update", return_value=True, + ), patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", + TEST_DEVICE_LIST, ), patch( "homeassistant.components.motion_blinds.async_setup_entry", return_value=True ): From 6b743c3d166340905b33da30648c16c77f2f4cec Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 24 Dec 2020 00:03:44 +0000 Subject: [PATCH 212/302] [ci skip] Translation update --- .../components/abode/translations/nl.json | 3 + .../components/abode/translations/sl.json | 17 +++++ .../accuweather/translations/sl.json | 8 +++ .../components/airly/translations/sl.json | 5 ++ .../components/apple_tv/translations/sl.json | 64 +++++++++++++++++++ .../binary_sensor/translations/nl.json | 13 ++++ .../components/bsblan/translations/sl.json | 4 +- .../device_tracker/translations/nl.json | 4 ++ .../fireservicerota/translations/sl.json | 29 +++++++++ .../components/gios/translations/sl.json | 5 ++ .../homeassistant/translations/nl.json | 3 + .../components/homekit/translations/nl.json | 5 ++ .../components/hyperion/translations/sl.json | 53 +++++++++++++++ .../components/ipma/translations/sl.json | 5 ++ .../components/kulersky/translations/sl.json | 13 ++++ .../components/lovelace/translations/nl.json | 10 +++ .../mobile_app/translations/nl.json | 5 ++ .../mobile_app/translations/sl.json | 5 ++ .../motion_blinds/translations/sl.json | 9 +++ .../components/neato/translations/sl.json | 15 ++++- .../components/nest/translations/cs.json | 4 ++ .../components/nest/translations/sl.json | 15 ++++- .../components/ozw/translations/sl.json | 11 ++++ .../components/point/translations/sl.json | 3 +- .../recollect_waste/translations/sl.json | 10 +++ .../components/sensor/translations/nl.json | 8 ++- .../components/solaredge/translations/sl.json | 7 +- .../speedtestdotnet/translations/nl.json | 3 +- .../components/spotify/translations/sl.json | 5 ++ .../tellduslive/translations/sl.json | 3 +- .../components/tile/translations/nl.json | 13 +++- .../components/toon/translations/sl.json | 3 +- .../components/tuya/translations/sl.json | 3 + 33 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sl.json create mode 100644 homeassistant/components/apple_tv/translations/sl.json create mode 100644 homeassistant/components/fireservicerota/translations/sl.json create mode 100644 homeassistant/components/hyperion/translations/sl.json create mode 100644 homeassistant/components/kulersky/translations/sl.json create mode 100644 homeassistant/components/lovelace/translations/nl.json create mode 100644 homeassistant/components/motion_blinds/translations/sl.json diff --git a/homeassistant/components/abode/translations/nl.json b/homeassistant/components/abode/translations/nl.json index a054d863c9..9177b1deb7 100644 --- a/homeassistant/components/abode/translations/nl.json +++ b/homeassistant/components/abode/translations/nl.json @@ -14,6 +14,9 @@ "mfa_code": "MFA-code (6-cijfers)" } }, + "reauth_confirm": { + "title": "Vul uw Abode-inloggegevens in" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/abode/translations/sl.json b/homeassistant/components/abode/translations/sl.json index aa54e582af..3f6a142e28 100644 --- a/homeassistant/components/abode/translations/sl.json +++ b/homeassistant/components/abode/translations/sl.json @@ -1,9 +1,26 @@ { "config": { "abort": { + "reauth_successful": "Ponovno overjanje je uspelo", "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode." }, + "error": { + "invalid_mfa_code": "Napa\u010dna MFA koda" + }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA koda (6 \u0161tevilk)" + }, + "title": "Vnesite MFA kodo za Abode" + }, + "reauth_confirm": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Vnesite podatke za prijavo v Abode" + }, "user": { "data": { "password": "Geslo", diff --git a/homeassistant/components/accuweather/translations/sl.json b/homeassistant/components/accuweather/translations/sl.json new file mode 100644 index 0000000000..f41ee93aef --- /dev/null +++ b/homeassistant/components/accuweather/translations/sl.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "can_reach_server": "Dostop do AccuWeather stre\u017enika", + "remaining_requests": "Preostalo dovoljenih zahtevkov" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sl.json b/homeassistant/components/airly/translations/sl.json index 71bea5a4d8..e1c8950139 100644 --- a/homeassistant/components/airly/translations/sl.json +++ b/homeassistant/components/airly/translations/sl.json @@ -18,5 +18,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dostop do Airly stre\u017enika" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sl.json b/homeassistant/components/apple_tv/translations/sl.json new file mode 100644 index 0000000000..997d60402c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/sl.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Naprava je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", + "backoff": "Naprav v tem trenutku ne sprejema zahtev za seznanitev (morda ste preve\u010dkrat vnesli napa\u010den PIN). Pokusitve znova kasneje.", + "device_did_not_pair": "Iz te naprave ni bilo poskusov zaklju\u010diti seznanjanja.", + "invalid_config": "Namestitev te naprave ni bila zaklju\u010dena. Poskusite ponovno.", + "no_devices_found": "Ni najdenih naprav v omre\u017eju", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "invalid_auth": "Napaka pri overjanju", + "no_devices_found": "Ni najdenih naprav v omre\u017eju", + "no_usable_service": "Najdena je bila naprava, za katero ni znan na\u010din povezovanja. \u010ce boste \u0161e vedno videli to sporo\u010dilo, poskusite dolo\u010diti IP naslov ali pa ponovno za\u017eenite Apple TV.", + "unknown": "Nepri\u010dakovana napaka" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "V Home Assistant nameravate dodati Apple TV z imenom `{name}`.\n\n**Za dokon\u010danje postopka boste morda morali ve\u010dkrat vnesti PIN kodo**\n\nS to integracijo ne boste mogli ugasniti svojega Apple TV. Ugasnjena bosta zgolj medijski predvajalnik in Home Assistant!", + "title": "Potrdite dodajanje Apple TV" + }, + "pair_no_pin": { + "description": "Protokol '{protocol}` zahteva seznanitev. Vnesite PIN {pin}, ki je prikazan na Apple TV.", + "title": "Seznanjanje" + }, + "pair_with_pin": { + "data": { + "pin": "PIN koda" + }, + "description": "Protokol '{protocol}` zahteva seznanitev. Vnesite PIN, ki je prikazan na zaslonu. Vodilnih ni\u010del ne vna\u0161ajte - vnesite 123, \u010de je prikazano 0123.", + "title": "Seznanjanje" + }, + "reconfigure": { + "description": "Ta Apple TV ima nekaj te\u017eav in mora biti ponovno konfiguriran.", + "title": "Ponovna namestitev naprave" + }, + "service_problem": { + "description": "Pri usklajevanju protokola `{protocol}` je pri\u0161lo do te\u017eave. Ta bo prezrta.", + "title": "Naprave ni mogo\u010de dodati" + }, + "user": { + "data": { + "device_input": "Naprava" + }, + "description": "Za\u010dnite z vnosom imena naprave (npr. kuhinja ali splanica) ali IP naslova Apple TV, ki bi ga radi dodali. \u010ce so katere naprave bile najdene samodejno v omre\u017eju, so prikazane spodaj.\n\n\u010ce ne vidite svoje naprave ali imate te\u017eave, poskusite dolo\u010diti nov IP.\n\n{devices}", + "title": "Namesti nov Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Ne vkpaljajte naprave ob zagonu Home Assistant-a" + }, + "description": "Konfiguracija splo\u0161nih nastavitev naprave" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index dc8ff4ec51..36dbb530fd 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -98,6 +98,10 @@ "off": "Normaal", "on": "Laag" }, + "battery_charging": { + "off": "Niet aan het opladen", + "on": "Opladen" + }, "cold": { "off": "Normaal", "on": "Koud" @@ -123,6 +127,7 @@ "on": "Heet" }, "light": { + "off": "Geen licht", "on": "Licht gedetecteerd" }, "lock": { @@ -137,6 +142,10 @@ "off": "Niet gedetecteerd", "on": "Gedetecteerd" }, + "moving": { + "off": "Niet bewegend", + "on": "In beweging" + }, "occupancy": { "off": "Niet gedetecteerd", "on": "Gedetecteerd" @@ -145,6 +154,10 @@ "off": "Gesloten", "on": "Open" }, + "plug": { + "off": "Unplugged", + "on": "Ingeplugd" + }, "presence": { "off": "Afwezig", "on": "Thuis" diff --git a/homeassistant/components/bsblan/translations/sl.json b/homeassistant/components/bsblan/translations/sl.json index 2bf2dd68b4..8eaa5185eb 100644 --- a/homeassistant/components/bsblan/translations/sl.json +++ b/homeassistant/components/bsblan/translations/sl.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "port": "Vrata" + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime" } } } diff --git a/homeassistant/components/device_tracker/translations/nl.json b/homeassistant/components/device_tracker/translations/nl.json index 99c0652d98..a28c8bdbbb 100644 --- a/homeassistant/components/device_tracker/translations/nl.json +++ b/homeassistant/components/device_tracker/translations/nl.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} is thuis", "is_not_home": "{entity_name} is niet thuis" + }, + "trigger_type": { + "enters": "{entity_name} gaat een zone binnen", + "leaves": "{entity_name} verlaat een zone" } }, "state": { diff --git a/homeassistant/components/fireservicerota/translations/sl.json b/homeassistant/components/fireservicerota/translations/sl.json new file mode 100644 index 0000000000..e38e7f9916 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun je \u017ee overjen", + "reauth_successful": "Ponovno overjanje je bilo uspe\u0161no" + }, + "create_entry": { + "default": "Uspe\u0161na overitev" + }, + "error": { + "invalid_auth": "Napaka pri overjanju" + }, + "step": { + "reauth": { + "data": { + "password": "Geslo" + }, + "description": "Overitveni \u017eetoni niso ve\u010d veljavni, ponovno se prijavite, da jih znova ustvarite." + }, + "user": { + "data": { + "password": "Geslo", + "url": "Spletna stran", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/sl.json b/homeassistant/components/gios/translations/sl.json index f01728783c..4bbc28bfed 100644 --- a/homeassistant/components/gios/translations/sl.json +++ b/homeassistant/components/gios/translations/sl.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (glavni poljski in\u0161pektorat za varstvo okolja)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dostop do GIOS stre\u017enika." + } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 277c044c53..338a019019 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -4,6 +4,9 @@ "dev": "Ontwikkeling", "docker": "Docker", "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "Type installatie", "os_version": "Versie van het besturingssysteem", "python_version": "Python versie", "supervisor": "Supervisor", diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index e2607c5f36..2733d6bd12 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -35,6 +35,11 @@ "description": "Controleer alle camera's die native H.264-streams ondersteunen. Als de camera geen H.264-stream uitvoert, transcodeert het systeem de video naar H.264 voor HomeKit. Transcodering vereist een performante CPU en het is onwaarschijnlijk dat dit werkt op computers met \u00e9\u00e9n bord.", "title": "Selecteer de videocodec van de camera." }, + "include_exclude": { + "data": { + "entities": "Entiteiten" + } + }, "init": { "data": { "include_domains": "Op te nemen domeinen", diff --git a/homeassistant/components/hyperion/translations/sl.json b/homeassistant/components/hyperion/translations/sl.json new file mode 100644 index 0000000000..6829f43011 --- /dev/null +++ b/homeassistant/components/hyperion/translations/sl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Storitev je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", + "auth_new_token_not_granted_error": "Uporabni\u0161ki vmesnik Hyperion UI ni potrdil novo ustvarjenega \u017eetona", + "auth_new_token_not_work_error": "Overjanje s pomo\u010djo novo ustvarjenega \u017eetona ni uspelo", + "auth_required_error": "Ni mogo\u010de dolo\u010diti ali je overjanje potrebno", + "cannot_connect": "Povezovanje ni bilo uspe\u0161no", + "no_id": "Hyperion Ambilight instanca ni prijavila tega id", + "reauth_successful": "Ponovno overjanje je uspelo." + }, + "error": { + "cannot_connect": "Neuspelo povezovanje", + "invalid_access_token": "Neveljaven \u017eeton za dostop" + }, + "step": { + "auth": { + "data": { + "create_token": "Samodejno ustvari nov \u017eeton", + "token": "ali zagotovite \u017ee obstoje\u010di \u017eeton" + }, + "description": "Nastavite overitev za Hyperion Ambilight stre\u017enik" + }, + "confirm": { + "description": "\u017delite dodati ta Hyperion Ambilight v Home Assistant?\n\n**Gostitelj:** {host}\n**Vrata:** {port}\n**ID**: {id}", + "title": "Potrdi dodajanje storitve Hyperion Ambilight" + }, + "create_token": { + "description": "Izberite **Posreduj*, \u010de \u017eelite zahtevati nov overitveni \u017eeton. Preusmerjeni boste na uporabni\u0161ki vmesnik Hyperion, da potrdite zahtevek. Prepri\u010dajte se, da je prikazani id \"{auth_id}\"", + "title": "Samodejno ustvari nov overitveni \u017eeton" + }, + "create_token_external": { + "title": "Sprejmi nov \u017eeton v uporabni\u0161kem vmesniku Hyperion" + }, + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Prednostna raba Hyperiona za barve in u\u010dinke" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/sl.json b/homeassistant/components/ipma/translations/sl.json index d42b858157..8c0c5441a9 100644 --- a/homeassistant/components/ipma/translations/sl.json +++ b/homeassistant/components/ipma/translations/sl.json @@ -15,5 +15,10 @@ "title": "Lokacija" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API kon\u010dna to\u010dka je dosegljiva" + } } } \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/sl.json b/homeassistant/components/kulersky/translations/sl.json new file mode 100644 index 0000000000..0108cb98d6 --- /dev/null +++ b/homeassistant/components/kulersky/translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav.", + "single_instance_allowed": "Je \u017ee name\u0161\u010deno. Mo\u017ena je le ena konfiguracija." + }, + "step": { + "confirm": { + "description": "\u017delite pri\u010deti namestitev?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/nl.json b/homeassistant/components/lovelace/translations/nl.json new file mode 100644 index 0000000000..ca8388f5be --- /dev/null +++ b/homeassistant/components/lovelace/translations/nl.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Dashboards", + "mode": "Modus", + "resources": "Bronnen", + "views": "Weergaven" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/nl.json b/homeassistant/components/mobile_app/translations/nl.json index 9d5bdcfa20..17a20705cd 100644 --- a/homeassistant/components/mobile_app/translations/nl.json +++ b/homeassistant/components/mobile_app/translations/nl.json @@ -8,5 +8,10 @@ "description": "Wilt u de Mobile App component instellen?" } } + }, + "device_automation": { + "action_type": { + "notify": "Stuur een notificatie" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sl.json b/homeassistant/components/mobile_app/translations/sl.json index 777776ce42..e0810147cd 100644 --- a/homeassistant/components/mobile_app/translations/sl.json +++ b/homeassistant/components/mobile_app/translations/sl.json @@ -8,5 +8,10 @@ "description": "Ali \u017eelite nastaviti komponento aplikacije Mobile App?" } } + }, + "device_automation": { + "action_type": { + "notify": "Po\u0161lji obvestilo" + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sl.json b/homeassistant/components/motion_blinds/translations/sl.json new file mode 100644 index 0000000000..bb61b03520 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/sl.json b/homeassistant/components/neato/translations/sl.json index 3ab2d0fd09..96af3b0453 100644 --- a/homeassistant/components/neato/translations/sl.json +++ b/homeassistant/components/neato/translations/sl.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "\u017de konfigurirano" + "already_configured": "\u017de konfigurirano", + "authorize_url_timeout": "\u010casovna omejitev pri ustvarjanju overitvenega URL je potekla.", + "missing_configuration": "Ta komponenta ni konfigurirana. Sledite dokumentaciji.", + "no_url_available": "URL ni na voljo. Za ve\u010d podatkov o tej napaki preverite [razdelek za pomo\u010d]({docs_url})", + "reauth_successful": "Ponovno overjanje je uspelo" }, "create_entry": { "default": "Glejte [neato dokumentacija] ({docs_url})." }, "step": { + "pick_implementation": { + "title": "Izberite na\u010din overjanja" + }, + "reauth_confirm": { + "title": "Bi radi zagnali namestitev?" + }, "user": { "data": { "password": "Geslo", @@ -17,5 +27,6 @@ "title": "Podatki o ra\u010dunu Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index 9ab94c993e..843ce98330 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, @@ -34,6 +35,9 @@ }, "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" } } }, diff --git a/homeassistant/components/nest/translations/sl.json b/homeassistant/components/nest/translations/sl.json index 4af5404c63..25660b4805 100644 --- a/homeassistant/components/nest/translations/sl.json +++ b/homeassistant/components/nest/translations/sl.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", - "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla." + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "reauth_successful": "Ponovna overitev je uspela.", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." }, "error": { "internal_error": "Notranja napaka pri preverjanju kode", @@ -23,7 +25,18 @@ }, "description": "\u010ce \u017eelite povezati svoj ra\u010dun Nest, [pooblastite svoj ra\u010dun]({url}). \n\n Po odobritvi kopirajte in prilepite podano kodo PIN.", "title": "Pove\u017eite Nest ra\u010dun" + }, + "reauth_confirm": { + "description": "Potrebna je ponovna overitev integracije" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Zaznano je gibanje", + "camera_person": "Zaznana je oseba", + "camera_sound": "Zaznan je zvok", + "doorbell_chime": "Zvonec je pritisnjen" + } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json index c4d67feb5b..8da77910c3 100644 --- a/homeassistant/components/ozw/translations/sl.json +++ b/homeassistant/components/ozw/translations/sl.json @@ -1,9 +1,20 @@ { "config": { "abort": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", "mqtt_required": "Integracija MQTT ni nastavljena" }, + "progress": { + "install_addon": "Po\u010dakajte, da se namestitev dodatka OpenZWave zaklju\u010di. To lahko traja ve\u010d minut." + }, "step": { + "hassio_confirm": { + "title": "Namestite OpenZWave integracijo z OpenZWave dodatkom." + }, + "install_addon": { + "title": "Namestitev dodatka OpenZWave se je za\u010dela" + }, "on_supervisor": { "title": "Izberite na\u010din povezave" } diff --git a/homeassistant/components/point/translations/sl.json b/homeassistant/components/point/translations/sl.json index 2fd65bb972..3c928935cc 100644 --- a/homeassistant/components/point/translations/sl.json +++ b/homeassistant/components/point/translations/sl.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", "external_setup": "To\u010dka uspe\u0161no konfigurirana iz drugega toka.", - "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Point. [Preberite navodila](https://www.home-assistant.io/components/point/)." + "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Point. [Preberite navodila](https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." }, "create_entry": { "default": "Uspe\u0161no overjen z Minut-om za va\u0161e Point naprave" diff --git a/homeassistant/components/recollect_waste/translations/sl.json b/homeassistant/components/recollect_waste/translations/sl.json index cae09d7762..480780db0a 100644 --- a/homeassistant/components/recollect_waste/translations/sl.json +++ b/homeassistant/components/recollect_waste/translations/sl.json @@ -10,5 +10,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u010ce je to mogo\u010de, uporabi prijazna imena za vrste pobiranja." + }, + "title": "Nastavi ponovno rabo zavr\u017eenega" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 7b4c1e71d5..869599296d 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -2,17 +2,23 @@ "device_automation": { "condition_type": { "is_battery_level": "Huidige batterijniveau {entity_name}", + "is_current": "Huidige {entity_name} stroom", + "is_energy": "Huidige {entity_name} energie", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", "is_power": "Huidige {entity_name}\nvermogen", + "is_power_factor": "Huidige {entity_name} vermogensfactor", "is_pressure": "Huidige {entity_name} druk", "is_signal_strength": "Huidige {entity_name} signaalsterkte", "is_temperature": "Huidige {entity_name} temperatuur", "is_timestamp": "Huidige {entity_name} tijdstip", - "is_value": "Huidige {entity_name} waarde" + "is_value": "Huidige {entity_name} waarde", + "is_voltage": "Huidige {entity_name} spanning" }, "trigger_type": { "battery_level": "{entity_name} batterijniveau gewijzigd", + "current": "{entity_name} huidige wijzigingen", + "energy": "{entity_name} energieveranderingen", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", "power": "{entity_name} vermogen gewijzigd", diff --git a/homeassistant/components/solaredge/translations/sl.json b/homeassistant/components/solaredge/translations/sl.json index 3f6e78fd3b..3414e37a65 100644 --- a/homeassistant/components/solaredge/translations/sl.json +++ b/homeassistant/components/solaredge/translations/sl.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", "site_exists": "Ta site_id je \u017ee nastavljen" }, "error": { - "site_exists": "Ta site_id je \u017ee nastavljen" + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "could_not_connect": "Ni se bilo mogo\u010de povezati s Solaredge API", + "invalid_api_key": "Neveljaven API klju\u010d", + "site_exists": "Ta site_id je \u017ee nastavljen", + "site_not_active": "Stran ni aktivna" }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 703ac8614c..0c0c184b5f 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", + "wrong_server_id": "Server-ID is niet geldig" } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/sl.json b/homeassistant/components/spotify/translations/sl.json index 0b13821124..a56222e22e 100644 --- a/homeassistant/components/spotify/translations/sl.json +++ b/homeassistant/components/spotify/translations/sl.json @@ -12,5 +12,10 @@ "title": "Izberite na\u010din preverjanja pristnosti" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Kon\u010dna to\u010dka Spotify API je dosegljiva" + } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/sl.json b/homeassistant/components/tellduslive/translations/sl.json index 9feea6d628..ec94501527 100644 --- a/homeassistant/components/tellduslive/translations/sl.json +++ b/homeassistant/components/tellduslive/translations/sl.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", - "unknown": "Pri\u0161lo je do neznane napake" + "unknown": "Pri\u0161lo je do neznane napake", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." }, "step": { "auth": { diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index 31529d69a2..26c5726868 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -7,7 +7,18 @@ "user": { "data": { "password": "Wachtwoord" - } + }, + "title": "Tegel configureren" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Toon inactieve tegels" + }, + "title": "Tegel configureren" } } } diff --git a/homeassistant/components/toon/translations/sl.json b/homeassistant/components/toon/translations/sl.json index 1883a5ab05..3a015b5ad6 100644 --- a/homeassistant/components/toon/translations/sl.json +++ b/homeassistant/components/toon/translations/sl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_agreements": "Ta ra\u010dun nima prikazov Toon." + "no_agreements": "Ta ra\u010dun nima prikazov Toon.", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json index 4879603af6..b07ad70ada 100644 --- a/homeassistant/components/tuya/translations/sl.json +++ b/homeassistant/components/tuya/translations/sl.json @@ -1,5 +1,8 @@ { "options": { + "abort": { + "cannot_connect": "Povezovanje ni uspelo." + }, "error": { "dev_not_config": "Vrsta naprave ni nastavljiva", "dev_not_found": "Naprave ni mogo\u010de najti" From 2d131823ce3867de7df09c023182a804b8a29cb3 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 24 Dec 2020 01:24:11 +0000 Subject: [PATCH 213/302] Fix filter sensor None state (#44439) Co-authored-by: Franck Nijhof --- homeassistant/components/filter/sensor.py | 18 +++++- tests/components/filter/test_sensor.py | 73 ++++++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 72300c1621..97d2b594cc 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -197,7 +197,15 @@ class SensorFilter(Entity): @callback def _update_filter_sensor_state(self, new_state, update_ha=True): """Process device state changes.""" - if new_state is None or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + if new_state is None: + _LOGGER.warning( + "While updating filter %s, the new_state is None", self._name + ) + self._state = None + self.async_write_ha_state() + return + + if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: self._state = new_state.state self.async_write_ha_state() return @@ -218,7 +226,11 @@ class SensorFilter(Entity): return temp_state = filtered_state except ValueError: - _LOGGER.error("Could not convert state: %s to number", self._state) + _LOGGER.error( + "Could not convert state: %s (%s) to number", + new_state.state, + type(new_state.state), + ) return self._state = temp_state.state @@ -425,7 +437,7 @@ class Filter: """Implement a common interface for filters.""" fstate = FilterState(new_state) if self._only_numbers and not isinstance(fstate.state, Number): - raise ValueError + raise ValueError(f"State <{fstate.state}> is not a Number") filtered = self._filter_state(fstate) filtered.set_precision(self.precision) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 8b779696a7..1c3b4b0d67 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -15,7 +15,7 @@ from homeassistant.components.filter.sensor import ( TimeThrottleFilter, ) from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE -from homeassistant.const import SERVICE_RELOAD, STATE_UNAVAILABLE +from homeassistant.const import SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -136,6 +136,71 @@ async def test_chain_history(hass, values, missing=False): assert "17.05" == state.state +async def test_source_state_none(hass, values): + """Test is source sensor state is null and sets state to STATE_UNKNOWN.""" + await async_init_recorder_component(hass) + + config = { + "sensor": [ + { + "platform": "template", + "sensors": { + "template_test": { + "value_template": "{{ states.sensor.test_state.state }}" + } + }, + }, + { + "platform": "filter", + "name": "test", + "entity_id": "sensor.template_test", + "filters": [ + { + "filter": "time_simple_moving_average", + "window_size": "00:01", + "precision": "2", + } + ], + }, + ] + } + await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", 0) + + await hass.async_block_till_done() + state = hass.states.get("sensor.template_test") + assert state.state == "0" + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state.state == "0.0" + + # Force Template Reload + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "template/sensor_configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "template", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + # Template state gets to None + state = hass.states.get("sensor.template_test") + assert state is None + + # Filter sensor ignores None state setting state to STATE_UNKNOWN + state = hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + + async def test_chain_history_missing(hass, values): """Test if filter chaining works when recorder is enabled but the source is not recorded.""" await test_chain_history(hass, values, missing=True) @@ -239,6 +304,12 @@ async def test_invalid_state(hass): state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE + hass.states.async_set("sensor.test_monitored", "invalid") + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + async def test_outlier(values): """Test if outlier filter works.""" From 677fc6e2bb4a844a3230e6f86cfeaaddab6494d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Dec 2020 17:23:26 -1000 Subject: [PATCH 214/302] Translate siri requests to turn on thermostats to valid targets (#44236) Siri always requests auto mode even when the device does not support auto which results in the thermostat failing to turn on as success is assumed. We now determine the heat cool target mode based on the current temp, target temp, and supported modes to ensure the thermostat will reach the requested target temp. --- .../components/homekit/type_thermostats.py | 92 +++-- .../homekit/test_type_thermostats.py | 350 ++++++++++++++---- 2 files changed, 356 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 6d0f5f22d7..54e2e9f92a 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -89,6 +89,20 @@ HC_HEAT_COOL_HEAT = 1 HC_HEAT_COOL_COOL = 2 HC_HEAT_COOL_AUTO = 3 +HC_HEAT_COOL_PREFER_HEAT = [ + HC_HEAT_COOL_AUTO, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_COOL, + HC_HEAT_COOL_OFF, +] + +HC_HEAT_COOL_PREFER_COOL = [ + HC_HEAT_COOL_AUTO, + HC_HEAT_COOL_COOL, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_OFF, +] + HC_MIN_TEMP = 10 HC_MAX_TEMP = 38 @@ -236,7 +250,7 @@ class Thermostat(HomeAccessory): state = self.hass.states.get(self.entity_id) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - hvac_mode = self.hass.states.get(self.entity_id).state + hvac_mode = state.state homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] if CHAR_TARGET_HEATING_COOLING in char_values: @@ -244,19 +258,37 @@ class Thermostat(HomeAccessory): # Ignore it if its the same mode if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode: target_hc = char_values[CHAR_TARGET_HEATING_COOLING] - if target_hc in self.hc_homekit_to_hass: - service = SERVICE_SET_HVAC_MODE_THERMOSTAT - hass_value = self.hc_homekit_to_hass[target_hc] - params = {ATTR_HVAC_MODE: hass_value} - events.append( - f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" - ) - else: - _LOGGER.warning( - "The entity: %s does not have a %s mode", - self.entity_id, - target_hc, - ) + if target_hc not in self.hc_homekit_to_hass: + # If the target heating cooling state we want does not + # exist on the device, we have to sort it out + # based on the the current and target temperature since + # siri will always send HC_HEAT_COOL_AUTO in this case + # and hope for the best. + hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE) + hc_current_temp = _get_current_temperature(state, self._unit) + hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT + if ( + hc_target_temp is not None + and hc_current_temp is not None + and hc_target_temp < hc_current_temp + ): + hc_fallback_order = HC_HEAT_COOL_PREFER_COOL + for hc_fallback in hc_fallback_order: + if hc_fallback in self.hc_homekit_to_hass: + _LOGGER.debug( + "Siri requested target mode: %s and the device does not support, falling back to %s", + target_hc, + hc_fallback, + ) + target_hc = hc_fallback + break + + service = SERVICE_SET_HVAC_MODE_THERMOSTAT + hass_value = self.hc_homekit_to_hass[target_hc] + params = {ATTR_HVAC_MODE: hass_value} + events.append( + f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" + ) if CHAR_TARGET_TEMPERATURE in char_values: hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] @@ -429,9 +461,8 @@ class Thermostat(HomeAccessory): self.char_current_heat_cool.set_value(homekit_hvac_action) # Update current temperature - current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if isinstance(current_temp, (int, float)): - current_temp = self._temperature_to_homekit(current_temp) + current_temp = _get_current_temperature(new_state, self._unit) + if current_temp is not None: if self.char_current_temp.value != current_temp: self.char_current_temp.set_value(current_temp) @@ -466,10 +497,8 @@ class Thermostat(HomeAccessory): self.char_heating_thresh_temp.set_value(heating_thresh) # Update target temperature - target_temp = new_state.attributes.get(ATTR_TEMPERATURE) - if isinstance(target_temp, (int, float)): - target_temp = self._temperature_to_homekit(target_temp) - elif features & SUPPORT_TARGET_TEMPERATURE_RANGE: + target_temp = _get_target_temperature(new_state, self._unit) + if target_temp is None and features & SUPPORT_TARGET_TEMPERATURE_RANGE: # Homekit expects a target temperature # even if the device does not support it hc_hvac_mode = self.char_target_heat_cool.value @@ -566,9 +595,8 @@ class WaterHeater(HomeAccessory): def async_update_state(self, new_state): """Update water_heater state after state change.""" # Update current and target temperature - temperature = new_state.attributes.get(ATTR_TEMPERATURE) - if isinstance(temperature, (int, float)): - temperature = temperature_to_homekit(temperature, self._unit) + temperature = _get_target_temperature(new_state, self._unit) + if temperature is not None: if temperature != self.char_current_temp.value: self.char_target_temp.set_value(temperature) @@ -606,3 +634,19 @@ def _get_temperature_range_from_state(state, unit, default_min, default_max): max_temp = min_temp return min_temp, max_temp + + +def _get_target_temperature(state, unit): + """Calculate the target temperature from a state.""" + target_temp = state.attributes.get(ATTR_TEMPERATURE) + if isinstance(target_temp, (int, float)): + return temperature_to_homekit(target_temp, unit) + return None + + +def _get_current_temperature(state, unit): + """Calculate the current temperature from a state.""" + target_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE) + if isinstance(target_temp, (int, float)): + return temperature_to_homekit(target_temp, unit) + return None diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e371fa6fe2..acb45bca85 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,6 +1,4 @@ """Test different accessory types: Thermostats.""" -from collections import namedtuple - from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -42,6 +40,14 @@ from homeassistant.components.homekit.const import ( PROP_MIN_STEP, PROP_MIN_VALUE, ) +from homeassistant.components.homekit.type_thermostats import ( + HC_HEAT_COOL_AUTO, + HC_HEAT_COOL_COOL, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_OFF, + Thermostat, + WaterHeater, +) from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER from homeassistant.const import ( ATTR_ENTITY_ID, @@ -57,24 +63,9 @@ from homeassistant.helpers import entity_registry from tests.async_mock import patch from tests.common import async_mock_service -from tests.components.homekit.common import patch_debounce -@pytest.fixture(scope="module") -def cls(): - """Patch debounce decorator during import of type_thermostats.""" - patcher = patch_debounce() - patcher.start() - _import = __import__( - "homeassistant.components.homekit.type_thermostats", - fromlist=["WaterHeater", "Thermostat"], - ) - patcher_tuple = namedtuple("Cls", ["water_heater", "thermostat"]) - yield patcher_tuple(thermostat=_import.Thermostat, water_heater=_import.WaterHeater) - patcher.stop() - - -async def test_thermostat(hass, hk_driver, cls, events): +async def test_thermostat(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -94,7 +85,7 @@ async def test_thermostat(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -414,7 +405,7 @@ async def test_thermostat(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3" -async def test_thermostat_auto(hass, hk_driver, cls, events): +async def test_thermostat_auto(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -436,7 +427,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -568,14 +559,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ) -async def test_thermostat_humidity(hass, hk_driver, cls, events): +async def test_thermostat_humidity(hass, hk_driver, events): """Test if accessory and HA are updated accordingly with humidity.""" entity_id = "climate.test" # support_auto = True hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -627,7 +618,7 @@ async def test_thermostat_humidity(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "35%" -async def test_thermostat_power_state(hass, hk_driver, cls, events): +async def test_thermostat_power_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -650,7 +641,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -747,7 +738,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): assert acc.char_target_heat_cool.value == 2 -async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): +async def test_thermostat_fahrenheit(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -762,7 +753,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): ) await hass.async_block_till_done() with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() await hass.async_block_till_done() @@ -856,13 +847,13 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "TargetTemperature to 24.0°C" -async def test_thermostat_get_temperature_range(hass, hk_driver, cls): +async def test_thermostat_get_temperature_range(hass, hk_driver): """Test if temperature range is evaluated correctly.""" entity_id = "climate.test" hass.states.async_set(entity_id, HVAC_MODE_OFF) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 2, None) hass.states.async_set( entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} @@ -878,13 +869,13 @@ async def test_thermostat_get_temperature_range(hass, hk_driver, cls): assert acc.get_temperature_range() == (15.5, 21.0) -async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): +async def test_thermostat_temperature_step_whole(hass, hk_driver): """Test climate device with single digit precision.""" entity_id = "climate.test" hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -893,7 +884,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 -async def test_thermostat_restore(hass, hk_driver, cls, events): +async def test_thermostat_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -919,7 +910,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) + acc = Thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) assert acc.category == 9 assert acc.get_temperature_range() == (7, 35) assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { @@ -929,7 +920,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events): "off", } - acc = cls.thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) + acc = Thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) assert acc.category == 9 assert acc.get_temperature_range() == (60.0, 70.0) assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { @@ -938,7 +929,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events): } -async def test_thermostat_hvac_modes(hass, hk_driver, cls): +async def test_thermostat_hvac_modes(hass, hk_driver): """Test if unsupported HVAC modes are deactivated in HomeKit.""" entity_id = "climate.test" @@ -947,7 +938,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls): ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -971,7 +962,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls): assert acc.char_target_heat_cool.value == 1 -async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): +async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver): """Test we get heat cool over auto.""" entity_id = "climate.test" @@ -990,7 +981,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1034,7 +1025,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): assert acc.char_target_heat_cool.value == 3 -async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls): +async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver): """Test we get auto when there is no heat cool.""" entity_id = "climate.test" @@ -1046,7 +1037,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1069,7 +1060,8 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls assert acc.char_target_heat_cool.value == 1 char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + await hass.async_block_till_done() hk_driver.set_characteristics( { HAP_REPR_CHARS: [ @@ -1090,7 +1082,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls assert acc.char_target_heat_cool.value == 3 -async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): +async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver): """Test if unsupported HVAC modes are deactivated in HomeKit.""" entity_id = "climate.test" @@ -1099,7 +1091,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1122,8 +1114,242 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + await hass.async_block_till_done() + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_HEAT, + }, + ] + }, + "mock_addr", + ) -async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + + +async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): + """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to heat.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_HEAT, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + hap = acc.char_target_heat_cool.to_HAP() + assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT] + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + await hass.async_block_till_done() + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + + +async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): + """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to cool.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_COOL, {ATTR_HVAC_MODES: [HVAC_MODE_COOL, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + hap = acc.char_target_heat_cool.to_HAP() + assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_COOL] + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + + +async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): + """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to heat or cool.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, + HVAC_MODE_COOL, + { + ATTR_CURRENT_TEMPERATURE: 30, + ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF], + }, + ) + + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + hap = acc.char_target_heat_cool.to_HAP() + assert hap["valid-values"] == [ + HC_HEAT_COOL_OFF, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_COOL, + ] + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_temp_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_temp_iid, + HAP_REPR_VALUE: 200, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + + +async def test_thermostat_hvac_modes_without_off(hass, hk_driver): """Test a thermostat that has no off.""" entity_id = "climate.test" @@ -1132,7 +1358,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1160,7 +1386,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): assert acc.char_target_heat_cool.value == 1 -async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, events): +async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events): """Test a thermostat that only supports a range.""" entity_id = "climate.test" @@ -1171,7 +1397,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE}, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1342,13 +1568,13 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 27.0°C" -async def test_water_heater(hass, hk_driver, cls, events): +async def test_water_heater(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() - acc = cls.water_heater(hass, hk_driver, "WaterHeater", entity_id, 2, None) + acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -1416,14 +1642,14 @@ async def test_water_heater(hass, hk_driver, cls, events): assert acc.char_target_heat_cool.value == 1 -async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): +async def test_water_heater_fahrenheit(hass, hk_driver, events): """Test if accessory and HA are update accordingly.""" entity_id = "water_heater.test" hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): - acc = cls.water_heater(hass, hk_driver, "WaterHeater", entity_id, 2, None) + acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -1448,13 +1674,13 @@ async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "140.0°F" -async def test_water_heater_get_temperature_range(hass, hk_driver, cls): +async def test_water_heater_get_temperature_range(hass, hk_driver): """Test if temperature range is evaluated correctly.""" entity_id = "water_heater.test" hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "WaterHeater", entity_id, 2, None) + acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) hass.states.async_set( entity_id, HVAC_MODE_HEAT, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} @@ -1470,7 +1696,7 @@ async def test_water_heater_get_temperature_range(hass, hk_driver, cls): assert acc.get_temperature_range() == (15.5, 21.0) -async def test_water_heater_restore(hass, hk_driver, cls, events): +async def test_water_heater_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -1492,7 +1718,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) + acc = Thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) assert acc.category == 9 assert acc.get_temperature_range() == (7, 35) assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { @@ -1501,7 +1727,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events): "Off", } - acc = cls.thermostat( + acc = WaterHeater( hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None ) assert acc.category == 9 @@ -1513,7 +1739,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events): } -async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, events): +async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, events): """Test if a thermostat that is not ready when we first see it.""" entity_id = "climate.test" @@ -1528,7 +1754,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1566,7 +1792,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, assert acc.char_display_units.value == 0 -async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events): +async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events): """Test if a thermostat that is not ready when we first see it that actually does not have off.""" entity_id = "climate.test" @@ -1581,7 +1807,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1619,7 +1845,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events assert acc.char_display_units.value == 0 -async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events): +async def test_thermostat_with_temp_clamps(hass, hk_driver, events): """Test that tempatures are clamped to valid values to prevent homekit crash.""" entity_id = "climate.test" @@ -1635,7 +1861,7 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() From d912e91e810d5b985766cb91978643467a7ee5c9 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 24 Dec 2020 03:25:14 -0800 Subject: [PATCH 215/302] Periodically attempt to discover new wemo devices (#44361) * Periodically attempt to discover new wemo devices * Set self._stop=None after stopping the periodic discovery task * Use the async_fire_time_changed test helper to simplify test_discovery * Stop the pywemo registry outside of the async loop * Add a comment describing why async_fire_time_changed is used --- homeassistant/components/wemo/__init__.py | 107 ++++++++++++++++------ tests/components/wemo/conftest.py | 4 + tests/components/wemo/test_init.py | 56 ++++++++++- 3 files changed, 137 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index d9d6a16ebf..32913c3722 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -11,9 +11,12 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later from .const import DOMAIN @@ -90,7 +93,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a wemo config entry.""" config = hass.data[DOMAIN].pop("config") @@ -98,14 +101,16 @@ async def async_setup_entry(hass, entry): registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() await hass.async_add_executor_job(registry.start) - def stop_wemo(event): + wemo_dispatcher = WemoDispatcher(entry) + wemo_discovery = WemoDiscovery(hass, wemo_dispatcher) + + async def async_stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") - registry.stop() + await hass.async_add_executor_job(registry.stop) + wemo_discovery.async_stop_discovery() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) - - devices = {} + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) static_conf = config.get(CONF_STATIC, []) if static_conf: @@ -116,28 +121,31 @@ async def async_setup_entry(hass, entry): for host, port in static_conf ] ): - if device is None: - continue - - devices.setdefault(device.serialnumber, device) + if device: + wemo_dispatcher.async_add_unique_device(hass, device) if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): - _LOGGER.debug("Scanning network for WeMo devices...") - for device in await hass.async_add_executor_job(pywemo.discover_devices): - devices.setdefault( - device.serialnumber, - device, - ) + await wemo_discovery.async_discover_and_schedule() - loaded_components = set() + return True - for device in devices.values(): - _LOGGER.debug( - "Adding WeMo device at %s:%i (%s)", - device.host, - device.port, - device.serialnumber, - ) + +class WemoDispatcher: + """Dispatch WeMo devices to the correct platform.""" + + def __init__(self, config_entry: ConfigEntry): + """Initialize the WemoDispatcher.""" + self._config_entry = config_entry + self._added_serial_numbers = set() + self._loaded_components = set() + + @callback + def async_add_unique_device( + self, hass: HomeAssistant, device: pywemo.WeMoDevice + ) -> None: + """Add a WeMo device to hass if it has not already been added.""" + if device.serialnumber in self._added_serial_numbers: + return component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN) @@ -146,11 +154,13 @@ async def async_setup_entry(hass, entry): # - Component is being loaded, add to backlog # - Component is loaded, backlog is gone, dispatch discovery - if component not in loaded_components: + if component not in self._loaded_components: hass.data[DOMAIN]["pending"][component] = [device] - loaded_components.add(component) + self._loaded_components.add(component) hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup( + self._config_entry, component + ) ) elif component in hass.data[DOMAIN]["pending"]: @@ -163,7 +173,48 @@ async def async_setup_entry(hass, entry): device, ) - return True + self._added_serial_numbers.add(device.serialnumber) + + +class WemoDiscovery: + """Use SSDP to discover WeMo devices.""" + + ADDITIONAL_SECONDS_BETWEEN_SCANS = 10 + MAX_SECONDS_BETWEEN_SCANS = 300 + + def __init__(self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher) -> None: + """Initialize the WemoDiscovery.""" + self._hass = hass + self._wemo_dispatcher = wemo_dispatcher + self._stop = None + self._scan_delay = 0 + + async def async_discover_and_schedule(self, *_) -> None: + """Periodically scan the network looking for WeMo devices.""" + _LOGGER.debug("Scanning network for WeMo devices...") + try: + for device in await self._hass.async_add_executor_job( + pywemo.discover_devices + ): + self._wemo_dispatcher.async_add_unique_device(self._hass, device) + finally: + # Run discovery more frequently after hass has just started. + self._scan_delay = min( + self._scan_delay + self.ADDITIONAL_SECONDS_BETWEEN_SCANS, + self.MAX_SECONDS_BETWEEN_SCANS, + ) + self._stop = async_call_later( + self._hass, + self._scan_delay, + self.async_discover_and_schedule, + ) + + @callback + def async_stop_discovery(self) -> None: + """Stop the periodic background scanning.""" + if self._stop: + self._stop() + self._stop = None def validate_static_config(host, port): diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 8eb8b0d9a3..573de21c69 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,4 +1,6 @@ """Fixtures for pywemo.""" +import asyncio + import pytest import pywemo @@ -26,9 +28,11 @@ def pywemo_registry_fixture(): registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) registry.callbacks = {} + registry.semaphore = asyncio.Semaphore(value=0) def on_func(device, type_filter, callback): registry.callbacks[device.name] = callback + registry.semaphore.release() registry.on.side_effect = on_func diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 87a6ff3955..2af91c0fe3 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -1,9 +1,17 @@ """Tests for the wemo component.""" -from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC +from datetime import timedelta + +import pywemo + +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, WemoDiscovery from homeassistant.components.wemo.const import DOMAIN from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from .conftest import MOCK_HOST, MOCK_PORT +from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_SERIAL_NUMBER + +from tests.async_mock import create_autospec, patch +from tests.common import async_fire_time_changed async def test_config_no_config(hass): @@ -87,3 +95,47 @@ async def test_static_config_with_invalid_host(hass): }, ) assert not setup_success + + +async def test_discovery(hass, pywemo_registry): + """Verify that discovery dispatches devices to the platform for setup.""" + + def create_device(counter): + """Create a unique mock Motion detector device for each counter value.""" + device = create_autospec(pywemo.Motion, instance=True) + device.host = f"{MOCK_HOST}_{counter}" + device.port = MOCK_PORT + counter + device.name = f"{MOCK_NAME}_{counter}" + device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}" + device.model_name = "Motion" + device.get_state.return_value = 0 # Default to Off + return device + + pywemo_devices = [create_device(0), create_device(1)] + # Setup the component and start discovery. + with patch( + "pywemo.discover_devices", return_value=pywemo_devices + ) as mock_discovery: + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} + ) + await pywemo_registry.semaphore.acquire() # Returns after platform setup. + mock_discovery.assert_called() + pywemo_devices.append(create_device(2)) + + # Test that discovery runs periodically and the async_dispatcher_send code works. + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1), + ) + await hass.async_block_till_done() + + # Verify that the expected number of devices were setup. + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 3 + + # Verify that hass stops cleanly. + await hass.async_stop() + await hass.async_block_till_done() From d385a51653a7199732abb34c76015f3360fbc82a Mon Sep 17 00:00:00 2001 From: Tomasz Pieczykolan Date: Thu, 24 Dec 2020 20:40:30 +0100 Subject: [PATCH 216/302] Fix the docstring in type_fans.py (#44511) --- homeassistant/components/homekit/type_fans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index d6231efe7a..1142a476bc 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -49,7 +49,7 @@ class Fan(HomeAccessory): """ def __init__(self, *args): - """Initialize a new Light accessory object.""" + """Initialize a new Fan accessory object.""" super().__init__(*args, category=CATEGORY_FAN) chars = [] state = self.hass.states.get(self.entity_id) From 19b957cd84e0666af8981604f408f770fd214d48 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Thu, 24 Dec 2020 20:42:56 +0100 Subject: [PATCH 217/302] Remove useless async_add_executor_job (#44496) --- homeassistant/components/somfy/climate.py | 20 +++++++++----------- homeassistant/components/somfy/cover.py | 20 +++++++++----------- homeassistant/components/somfy/sensor.py | 22 ++++++++++------------ homeassistant/components/somfy/switch.py | 20 +++++++++----------- 4 files changed, 37 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index c4abc12d24..00a2738f4f 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -49,19 +49,17 @@ HVAC_MODES_MAPPING = {HvacState.COOL: HVAC_MODE_COOL, HvacState.HEAT: HVAC_MODE_ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy climate platform.""" - def get_thermostats(): - """Retrieve thermostats.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - api = domain_data[API] + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] - return [ - SomfyClimate(coordinator, device_id, api) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] + climates = [ + SomfyClimate(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] - async_add_entities(await hass.async_add_executor_job(get_thermostats)) + async_add_entities(climates) class SomfyClimate(SomfyEntity, ClimateEntity): diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 4542506bec..e730855812 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -36,19 +36,17 @@ SUPPORTED_CATEGORIES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy cover platform.""" - def get_covers(): - """Retrieve covers.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - api = domain_data[API] + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] - return [ - SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC]) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] + covers = [ + SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC]) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] - async_add_entities(await hass.async_add_executor_job(get_covers)) + async_add_entities(covers) class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 3d7b1b8fc1..1becc929ad 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -12,21 +12,19 @@ SUPPORTED_CATEGORIES = {Category.HVAC.value} async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Somfy climate platform.""" + """Set up the Somfy sensor platform.""" - def get_thermostats(): - """Retrieve thermostats.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - api = domain_data[API] + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] - return [ - SomfyThermostatBatterySensor(coordinator, device_id, api) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] + sensors = [ + SomfyThermostatBatterySensor(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] - async_add_entities(await hass.async_add_executor_job(get_thermostats)) + async_add_entities(sensors) class SomfyThermostatBatterySensor(SomfyEntity): diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index d614776778..1432895336 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -11,19 +11,17 @@ from .const import API, COORDINATOR, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy switch platform.""" - def get_shutters(): - """Retrieve switches.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - api = domain_data[API] + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] - return [ - SomfyCameraShutter(coordinator, device_id, api) - for device_id, device in coordinator.data.items() - if Category.CAMERA.value in device.categories - ] + switches = [ + SomfyCameraShutter(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if Category.CAMERA.value in device.categories + ] - async_add_entities(await hass.async_add_executor_job(get_shutters), True) + async_add_entities(switches) class SomfyCameraShutter(SomfyEntity, SwitchEntity): From 6f2ed86c49b5721cff4f3678b5bc9e5db7f8c929 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 25 Dec 2020 00:03:46 +0000 Subject: [PATCH 218/302] [ci skip] Translation update --- homeassistant/components/abode/translations/th.json | 9 +++++++++ homeassistant/components/nest/translations/th.json | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 homeassistant/components/abode/translations/th.json diff --git a/homeassistant/components/abode/translations/th.json b/homeassistant/components/abode/translations/th.json new file mode 100644 index 0000000000..2b9eefdbb6 --- /dev/null +++ b/homeassistant/components/abode/translations/th.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "mfa": { + "title": "\u0e1b\u0e49\u0e2d\u0e19\u0e23\u0e2b\u0e31\u0e2a MFA \u0e08\u0e32\u0e01 Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/th.json b/homeassistant/components/nest/translations/th.json index 797aac8240..5f14558e2b 100644 --- a/homeassistant/components/nest/translations/th.json +++ b/homeassistant/components/nest/translations/th.json @@ -5,7 +5,17 @@ "data": { "code": "Pin code" } + }, + "reauth_confirm": { + "title": "\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e01\u0e32\u0e23\u0e1a\u0e39\u0e23\u0e13\u0e32\u0e01\u0e32\u0e23\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e04\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e2b\u0e27", + "camera_person": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e1a\u0e38\u0e04\u0e04\u0e25", + "camera_sound": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e40\u0e2a\u0e35\u0e22\u0e07" + } } } \ No newline at end of file From 9a5132054f486a2ccfcd57953c290eb8f9177856 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 25 Dec 2020 08:49:14 -0500 Subject: [PATCH 219/302] Support auto as Dyson fan on device state (#44472) * - Make the dyson integration report that the fan is on if its in AUTO or FAN states instead of only in FAN state * - Fix code style issue to be compliant with flake8 --- homeassistant/components/dyson/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 4d9fe2eba2..ca685f36a1 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -257,7 +257,7 @@ class DysonPureCoolLinkDevice(FanEntity): def is_on(self): """Return true if the entity is on.""" if self._device.state: - return self._device.state.fan_mode == "FAN" + return self._device.state.fan_mode in ["FAN", "AUTO"] return False @property From adf4eb0cc93668e3b5931c2ba62d17ed32cb8b9d Mon Sep 17 00:00:00 2001 From: Matt Bilodeau Date: Fri, 25 Dec 2020 10:37:30 -0500 Subject: [PATCH 220/302] Bump pywemo to 0.5.6 (#44440) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index e986913fc7..dc04926004 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.5.3"], + "requirements": ["pywemo==0.5.6"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 68e2c5187d..bddeb3096e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1892,7 +1892,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.3 +pywemo==0.5.6 # homeassistant.components.wilight pywilight==0.0.65 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f9c7134fd..0622f6a788 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.3 +pywemo==0.5.6 # homeassistant.components.wilight pywilight==0.0.65 From fa69daf5b3a4a825cabae3ac8b19523623f22c25 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 25 Dec 2020 21:49:30 +0100 Subject: [PATCH 221/302] Convert mpd component to use the async MPDClient (#44384) Upgrades python-mpd2 to 3.0.1. --- homeassistant/components/mpd/manifest.json | 2 +- homeassistant/components/mpd/media_player.py | 123 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 64 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index de7b8b8f0d..5e9b4f8e69 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -2,6 +2,6 @@ "domain": "mpd", "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", - "requirements": ["python-mpd2==1.0.0"], + "requirements": ["python-mpd2==3.0.1"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 280956c7dd..1e16675f7b 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -4,6 +4,7 @@ import logging import os import mpd +from mpd.asyncio import MPDClient import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -75,15 +76,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the MPD platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) password = config.get(CONF_PASSWORD) - device = MpdDevice(host, port, password, name) - add_entities([device], True) + entity = MpdDevice(host, port, password, name) + async_add_entities([entity], True) class MpdDevice(MediaPlayerEntity): @@ -108,17 +109,17 @@ class MpdDevice(MediaPlayerEntity): self._media_position = None # set up MPD client - self._client = mpd.MPDClient() + self._client = MPDClient() self._client.timeout = 30 self._client.idletimeout = None - def _connect(self): + async def _connect(self): """Connect to MPD.""" try: - self._client.connect(self.server, self.port) + await self._client.connect(self.server, self.port) if self.password is not None: - self._client.password(self.password) + await self._client.password(self.password) except mpd.ConnectionError: return @@ -133,10 +134,10 @@ class MpdDevice(MediaPlayerEntity): self._is_connected = False self._status = None - def _fetch_status(self): + async def _fetch_status(self): """Fetch status from MPD.""" - self._status = self._client.status() - self._currentsong = self._client.currentsong() + self._status = await self._client.status() + self._currentsong = await self._client.currentsong() position = self._status.get("elapsed") @@ -150,20 +151,20 @@ class MpdDevice(MediaPlayerEntity): self._media_position_updated_at = dt_util.utcnow() self._media_position = int(float(position)) - self._update_playlists() + await self._update_playlists() @property def available(self): """Return true if MPD is available and connected.""" return self._is_connected - def update(self): + async def async_update(self): """Get the latest data and update the state.""" try: if not self._is_connected: - self._connect() + await self._connect() - self._fetch_status() + await self._fetch_status() except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error: # Cleanly disconnect in case connection is not in valid state _LOGGER.debug("Error updating status: %s", error) @@ -282,27 +283,27 @@ class MpdDevice(MediaPlayerEntity): """Return the list of available input sources.""" return self._playlists - def select_source(self, source): + async def async_select_source(self, source): """Choose a different available playlist and play it.""" - self.play_media(MEDIA_TYPE_PLAYLIST, source) + await self.async_play_media(MEDIA_TYPE_PLAYLIST, source) @Throttle(PLAYLIST_UPDATE_INTERVAL) - def _update_playlists(self, **kwargs): + async def _update_playlists(self, **kwargs): """Update available MPD playlists.""" try: self._playlists = [] - for playlist_data in self._client.listplaylists(): + for playlist_data in await self._client.listplaylists(): self._playlists.append(playlist_data["playlist"]) except mpd.CommandError as error: self._playlists = None _LOGGER.warning("Playlists could not be updated: %s:", error) - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume of media player.""" if "volume" in self._status: - self._client.setvol(int(volume * 100)) + await self._client.setvol(int(volume * 100)) - def volume_up(self): + async def async_volume_up(self): """Service to send the MPD the command for volume up.""" if "volume" in self._status: current_volume = int(self._status["volume"]) @@ -310,48 +311,48 @@ class MpdDevice(MediaPlayerEntity): if current_volume <= 100: self._client.setvol(current_volume + 5) - def volume_down(self): + async def async_volume_down(self): """Service to send the MPD the command for volume down.""" if "volume" in self._status: current_volume = int(self._status["volume"]) if current_volume >= 0: - self._client.setvol(current_volume - 5) + await self._client.setvol(current_volume - 5) - def media_play(self): + async def async_media_play(self): """Service to send the MPD the command for play/pause.""" if self._status["state"] == "pause": - self._client.pause(0) + await self._client.pause(0) else: - self._client.play() + await self._client.play() - def media_pause(self): + async def async_media_pause(self): """Service to send the MPD the command for play/pause.""" - self._client.pause(1) + await self._client.pause(1) - def media_stop(self): + async def async_media_stop(self): """Service to send the MPD the command for stop.""" - self._client.stop() + await self._client.stop() - def media_next_track(self): + async def async_media_next_track(self): """Service to send the MPD the command for next track.""" - self._client.next() + await self._client.next() - def media_previous_track(self): + async def async_media_previous_track(self): """Service to send the MPD the command for previous track.""" - self._client.previous() + await self._client.previous() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Mute. Emulated with set_volume_level.""" if "volume" in self._status: if mute: self._muted_volume = self.volume_level - self.set_volume_level(0) + await self.async_set_volume_level(0) else: - self.set_volume_level(self._muted_volume) + await self.async_set_volume_level(self._muted_volume) self._muted = mute - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Send the media player the command for playing a playlist.""" _LOGGER.debug("Playing playlist: %s", media_id) if media_type == MEDIA_TYPE_PLAYLIST: @@ -360,14 +361,14 @@ class MpdDevice(MediaPlayerEntity): else: self._currentplaylist = None _LOGGER.warning("Unknown playlist name %s", media_id) - self._client.clear() - self._client.load(media_id) - self._client.play() + await self._client.clear() + await self._client.load(media_id) + await self._client.play() else: - self._client.clear() + await self._client.clear() self._currentplaylist = None - self._client.add(media_id) - self._client.play() + await self._client.add(media_id) + await self._client.play() @property def repeat(self): @@ -378,40 +379,40 @@ class MpdDevice(MediaPlayerEntity): return REPEAT_MODE_ALL return REPEAT_MODE_OFF - def set_repeat(self, repeat): + async def async_set_repeat(self, repeat): """Set repeat mode.""" if repeat == REPEAT_MODE_OFF: - self._client.repeat(0) - self._client.single(0) + await self._client.repeat(0) + await self._client.single(0) else: - self._client.repeat(1) + await self._client.repeat(1) if repeat == REPEAT_MODE_ONE: - self._client.single(1) + await self._client.single(1) else: - self._client.single(0) + await self._client.single(0) @property def shuffle(self): """Boolean if shuffle is enabled.""" return bool(int(self._status["random"])) - def set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" - self._client.random(int(shuffle)) + await self._client.random(int(shuffle)) - def turn_off(self): + async def async_turn_off(self): """Service to send the MPD the command to stop playing.""" - self._client.stop() + await self._client.stop() - def turn_on(self): + async def async_turn_on(self): """Service to send the MPD the command to start playing.""" - self._client.play() - self._update_playlists(no_throttle=True) + await self._client.play() + await self._update_playlists(no_throttle=True) - def clear_playlist(self): + async def async_clear_playlist(self): """Clear players playlist.""" - self._client.clear() + await self._client.clear() - def media_seek(self, position): + async def async_media_seek(self, position): """Send seek command.""" - self._client.seekcur(position) + await self._client.seekcur(position) diff --git a/requirements_all.txt b/requirements_all.txt index bddeb3096e..efdef2dc28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1786,7 +1786,7 @@ python-juicenet==1.0.1 python-miio==0.5.4 # homeassistant.components.mpd -python-mpd2==1.0.0 +python-mpd2==3.0.1 # homeassistant.components.mystrom python-mystrom==1.1.2 From dc79d71f39e66ab309ce708a0e01e690a925a00b Mon Sep 17 00:00:00 2001 From: Thibaut Date: Fri, 25 Dec 2020 22:29:23 +0100 Subject: [PATCH 222/302] Handle missing Somfy devices during update (#44425) * Handle missing devices during update * Raise error only if there was devices previously * Not final dot Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/somfy/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 78429dd1fe..2fc83ea71d 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from . import api @@ -92,6 +93,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def _update_all_devices(): """Update all the devices.""" devices = await hass.async_add_executor_job(data[API].get_devices) + previous_devices = data[COORDINATOR].data + # Sometimes Somfy returns an empty list. + if not devices and previous_devices: + raise UpdateFailed("No devices returned") return {dev.id: dev for dev in devices} coordinator = DataUpdateCoordinator( From 3a71b62de6c8dfa10834e7d4b2e052d0aa3317bc Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Sat, 26 Dec 2020 10:05:41 +0100 Subject: [PATCH 223/302] Update README.rst to avoid redirects (#44519) --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 0de30d43c6..cf8323d2e8 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Open source home automation that puts local control and privacy first. Powered b Check out `home-assistant.io `__ for `a demo `__, `installation instructions `__, -`tutorials `__ and `documentation `__. +`tutorials `__ and `documentation `__. |screenshot-states| @@ -14,8 +14,8 @@ Featured integrations |screenshot-components| -The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own -components `__. +The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own +components `__. If you run into issues while using Home Assistant or during development of a component, check the `Home Assistant help section `__ of our website for further help and information. From cfb02b5392196c4ea1ab5c3df2de4dbae8197d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 26 Dec 2020 13:49:44 +0200 Subject: [PATCH 224/302] Upgrade huawei-lte-api to 1.4.17 (#44499) Mostly to pave way for implementing network connection mode change services, such as PR #44380. https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.13 https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.14 https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.15 https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.16 https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.17 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index fd57478483..b0cd7bb8b8 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.2", - "huawei-lte-api==1.4.12", + "huawei-lte-api==1.4.17", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index efdef2dc28..4dfd64840b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,7 +784,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.12 +huawei-lte-api==1.4.17 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0622f6a788..47c92051e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -410,7 +410,7 @@ homematicip==0.13.0 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.12 +huawei-lte-api==1.4.17 # homeassistant.components.hyperion hyperion-py==0.6.1 From f8ab98d2e192fa0e1cd3d74b95f691944a423c6c Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 26 Dec 2020 07:53:34 -0500 Subject: [PATCH 225/302] Fix falsey comparisons in NWS weather (#44486) --- homeassistant/components/nws/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 69dac297b1..34c2909188 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -161,7 +161,7 @@ class NWSWeather(WeatherEntity): temp_c = None if self.observation: temp_c = self.observation.get("temperature") - if temp_c: + if temp_c is not None: return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) return None @@ -273,7 +273,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") - if wind_speed: + if wind_speed is not None: if self.is_metric: data[ATTR_FORECAST_WIND_SPEED] = round( convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) From f8df8b6b19e997ce1dd2490163d97fbf777a3820 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 26 Dec 2020 22:18:28 +0100 Subject: [PATCH 226/302] Add album art support in the mpd component (#44527) * Add album art support in the mpd component Uses `readpicture` to retrieve embedded artwork and `albumart` to acquire cover art located in the media directory. As the mpd component supports multiple different implementations (think mopidy, PI MusicBox, etc.) we check for the availability of each command before using them. Tested against mpd 0.22.3, which includes support for both. * fixup! Add album art support in the mpd component --- homeassistant/components/mpd/media_player.py | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 1e16675f7b..1273b720dd 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -1,5 +1,6 @@ """Support to interact with a Music Player Daemon.""" from datetime import timedelta +import hashlib import logging import os @@ -107,6 +108,7 @@ class MpdDevice(MediaPlayerEntity): self._muted_volume = 0 self._media_position_updated_at = None self._media_position = None + self._commands = None # set up MPD client self._client = MPDClient() @@ -163,6 +165,7 @@ class MpdDevice(MediaPlayerEntity): try: if not self._is_connected: await self._connect() + self._commands = list(await self._client.commands()) await self._fetch_status() except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error: @@ -252,6 +255,56 @@ class MpdDevice(MediaPlayerEntity): """Return the album of current playing media (Music track only).""" return self._currentsong.get("album") + @property + def media_image_hash(self): + """Hash value for media image.""" + file = self._currentsong.get("file") + if file: + return hashlib.sha256(file.encode("utf-8")).hexdigest()[:16] + + return None + + async def async_get_media_image(self): + """Fetch media image of current playing track.""" + file = self._currentsong.get("file") + if not file: + return None, None + + # not all MPD implementations and versions support the `albumart` and `fetchpicture` commands + can_albumart = "albumart" in self._commands + can_readpicture = "readpicture" in self._commands + + response = None + + # read artwork embedded into the media file + if can_readpicture: + try: + response = await self._client.readpicture(file) + except mpd.CommandError as error: + _LOGGER.warning( + "Retrieving artwork through `readpicture` command failed: %s", + error, + ) + + # read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded + if can_albumart and not response: + try: + response = await self._client.albumart(file) + except mpd.CommandError as error: + _LOGGER.warning( + "Retrieving artwork through `albumart` command failed: %s", + error, + ) + + if not response: + return None, None + + image = bytes(response.get("binary")) + mime = response.get( + "type", "image/png" + ) # readpicture has type, albumart does not + return (image, mime) + @property def volume_level(self): """Return the volume level.""" From 49d98236309e39a12a2b18142726da334bc5e457 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 26 Dec 2020 22:24:05 +0100 Subject: [PATCH 227/302] Bump pydeconz to version 77 (#44514) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c2846f8c57..22711b84b9 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==76"], + "requirements": ["pydeconz==77"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index 4dfd64840b..99f972dc59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1337,7 +1337,7 @@ pydaikin==2.4.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==76 +pydeconz==77 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47c92051e1..9385f6956d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ pycountry==19.8.18 pydaikin==2.4.0 # homeassistant.components.deconz -pydeconz==76 +pydeconz==77 # homeassistant.components.dexcom pydexcom==0.2.0 From 71c9007e065c4791152cd128974503db298e0380 Mon Sep 17 00:00:00 2001 From: aque0us Date: Sun, 27 Dec 2020 02:32:22 -0500 Subject: [PATCH 228/302] Add Olivia voice to Amazon Polly TTS (#44513) * Update tts.py Added Olivia Neural Voice * Correct linting Co-authored-by: Joakim Plate --- homeassistant/components/amazon_polly/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index d1c12e657f..fb9560832c 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -42,6 +42,7 @@ CONF_SAMPLE_RATE = "sample_rate" CONF_TEXT_TYPE = "text_type" SUPPORTED_VOICES = [ + "Olivia", # Female, Australian, Neural "Zhiyu", # Chinese "Mads", "Naja", # Danish From 1f27fb464420f84eae5a6b36022a9397edb827cb Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 27 Dec 2020 09:30:20 +0100 Subject: [PATCH 229/302] Fully remove Avri integration (#44478) * Fully remove Avri integration * Remove avri from .coveragerc --- .coveragerc | 2 - CODEOWNERS | 1 - .../components/avri/.translations/en.json | 24 ----- .../components/avri/.translations/nl.json | 24 ----- homeassistant/components/avri/__init__.py | 60 ----------- homeassistant/components/avri/config_flow.py | 76 -------------- homeassistant/components/avri/const.py | 8 -- homeassistant/components/avri/manifest.json | 13 --- homeassistant/components/avri/sensor.py | 99 ------------------- homeassistant/components/avri/strings.json | 23 ----- .../components/avri/translations/ar.json | 7 -- .../components/avri/translations/ca.json | 23 ----- .../components/avri/translations/cs.json | 23 ----- .../components/avri/translations/de.json | 20 ---- .../components/avri/translations/en.json | 23 ----- .../components/avri/translations/es.json | 23 ----- .../components/avri/translations/et.json | 23 ----- .../components/avri/translations/fr.json | 23 ----- .../components/avri/translations/it.json | 23 ----- .../components/avri/translations/ko.json | 23 ----- .../components/avri/translations/lb.json | 23 ----- .../components/avri/translations/nl.json | 10 -- .../components/avri/translations/no.json | 23 ----- .../components/avri/translations/pl.json | 23 ----- .../components/avri/translations/pt.json | 14 --- .../components/avri/translations/ru.json | 23 ----- .../components/avri/translations/zh-Hant.json | 23 ----- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 6 -- requirements_test_all.txt | 6 -- tests/components/avri/__init__.py | 1 - tests/components/avri/test_config_flow.py | 81 --------------- 32 files changed, 775 deletions(-) delete mode 100644 homeassistant/components/avri/.translations/en.json delete mode 100644 homeassistant/components/avri/.translations/nl.json delete mode 100644 homeassistant/components/avri/__init__.py delete mode 100644 homeassistant/components/avri/config_flow.py delete mode 100644 homeassistant/components/avri/const.py delete mode 100644 homeassistant/components/avri/manifest.json delete mode 100644 homeassistant/components/avri/sensor.py delete mode 100644 homeassistant/components/avri/strings.json delete mode 100644 homeassistant/components/avri/translations/ar.json delete mode 100644 homeassistant/components/avri/translations/ca.json delete mode 100644 homeassistant/components/avri/translations/cs.json delete mode 100644 homeassistant/components/avri/translations/de.json delete mode 100644 homeassistant/components/avri/translations/en.json delete mode 100644 homeassistant/components/avri/translations/es.json delete mode 100644 homeassistant/components/avri/translations/et.json delete mode 100644 homeassistant/components/avri/translations/fr.json delete mode 100644 homeassistant/components/avri/translations/it.json delete mode 100644 homeassistant/components/avri/translations/ko.json delete mode 100644 homeassistant/components/avri/translations/lb.json delete mode 100644 homeassistant/components/avri/translations/nl.json delete mode 100644 homeassistant/components/avri/translations/no.json delete mode 100644 homeassistant/components/avri/translations/pl.json delete mode 100644 homeassistant/components/avri/translations/pt.json delete mode 100644 homeassistant/components/avri/translations/ru.json delete mode 100644 homeassistant/components/avri/translations/zh-Hant.json delete mode 100644 tests/components/avri/__init__.py delete mode 100644 tests/components/avri/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f637b64149..64a22ef275 100644 --- a/.coveragerc +++ b/.coveragerc @@ -73,8 +73,6 @@ omit = homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py - homeassistant/components/avri/const.py - homeassistant/components/avri/sensor.py homeassistant/components/azure_devops/__init__.py homeassistant/components/azure_devops/const.py homeassistant/components/azure_devops/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index e5d67234f6..66c5801e39 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,7 +54,6 @@ homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland -homeassistant/components/avri/* @timvancann homeassistant/components/awair/* @ahayworth @danielsjf homeassistant/components/aws/* @awarecan homeassistant/components/axis/* @Kane610 diff --git a/homeassistant/components/avri/.translations/en.json b/homeassistant/components/avri/.translations/en.json deleted file mode 100644 index 83cd4232d4..0000000000 --- a/homeassistant/components/avri/.translations/en.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "This address is already configured." - }, - "error": { - "invalid_country_code": "Unknown 2 letter country code.", - "invalid_house_number": "Invalid house number." - }, - "step": { - "user": { - "data": { - "country_code": "2 Letter country code", - "house_number": "House number", - "house_number_extension": "House number extension", - "zip_code": "Zip code" - }, - "description": "Enter your address", - "title": "Avri" - } - } - }, - "title": "Avri" -} \ No newline at end of file diff --git a/homeassistant/components/avri/.translations/nl.json b/homeassistant/components/avri/.translations/nl.json deleted file mode 100644 index 22798b0968..0000000000 --- a/homeassistant/components/avri/.translations/nl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dit adres is reeds geconfigureerd." - }, - "error": { - "invalid_country_code": "Onbekende landcode", - "invalid_house_number": "Ongeldig huisnummer." - }, - "step": { - "user": { - "data": { - "country_code": "2 Letter landcode", - "house_number": "Huisnummer", - "house_number_extension": "Huisnummer toevoeging", - "zip_code": "Postcode" - }, - "description": "Vul je adres in.", - "title": "Avri" - } - } - }, - "title": "Avri" -} \ No newline at end of file diff --git a/homeassistant/components/avri/__init__.py b/homeassistant/components/avri/__init__.py deleted file mode 100644 index f3b659ddcc..0000000000 --- a/homeassistant/components/avri/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -"""The avri component.""" -import asyncio -from datetime import timedelta - -from avri.api import Avri - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import ( - CONF_COUNTRY_CODE, - CONF_HOUSE_NUMBER, - CONF_HOUSE_NUMBER_EXTENSION, - CONF_ZIP_CODE, - DOMAIN, -) - -PLATFORMS = ["sensor"] -SCAN_INTERVAL = timedelta(hours=4) - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Avri component.""" - hass.data[DOMAIN] = {} - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Avri from a config entry.""" - client = Avri( - postal_code=entry.data[CONF_ZIP_CODE], - house_nr=entry.data[CONF_HOUSE_NUMBER], - house_nr_extension=entry.data.get(CONF_HOUSE_NUMBER_EXTENSION), - country_code=entry.data[CONF_COUNTRY_CODE], - ) - - hass.data[DOMAIN][entry.entry_id] = client - - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/avri/config_flow.py b/homeassistant/components/avri/config_flow.py deleted file mode 100644 index 987b3679b3..0000000000 --- a/homeassistant/components/avri/config_flow.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Config flow for Avri component.""" -import pycountry -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_ID - -from .const import ( - CONF_COUNTRY_CODE, - CONF_HOUSE_NUMBER, - CONF_HOUSE_NUMBER_EXTENSION, - CONF_ZIP_CODE, - DEFAULT_COUNTRY_CODE, -) -from .const import DOMAIN # pylint:disable=unused-import - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_ZIP_CODE): str, - vol.Required(CONF_HOUSE_NUMBER): int, - vol.Optional(CONF_HOUSE_NUMBER_EXTENSION): str, - vol.Optional(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): str, - } -) - - -class AvriConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Avri config flow.""" - - VERSION = 1 - - async def _show_setup_form(self, errors=None): - """Show the setup form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - if user_input is None: - return await self._show_setup_form() - - zip_code = user_input[CONF_ZIP_CODE].replace(" ", "").upper() - - errors = {} - if user_input[CONF_HOUSE_NUMBER] <= 0: - errors[CONF_HOUSE_NUMBER] = "invalid_house_number" - return await self._show_setup_form(errors) - if not pycountry.countries.get(alpha_2=user_input[CONF_COUNTRY_CODE]): - errors[CONF_COUNTRY_CODE] = "invalid_country_code" - return await self._show_setup_form(errors) - - unique_id = ( - f"{zip_code}" - f" " - f"{user_input[CONF_HOUSE_NUMBER]}" - f'{user_input.get(CONF_HOUSE_NUMBER_EXTENSION, "")}' - ) - - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=unique_id, - data={ - CONF_ID: unique_id, - CONF_ZIP_CODE: zip_code, - CONF_HOUSE_NUMBER: user_input[CONF_HOUSE_NUMBER], - CONF_HOUSE_NUMBER_EXTENSION: user_input.get( - CONF_HOUSE_NUMBER_EXTENSION, "" - ), - CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], - }, - ) diff --git a/homeassistant/components/avri/const.py b/homeassistant/components/avri/const.py deleted file mode 100644 index dab3491b35..0000000000 --- a/homeassistant/components/avri/const.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Constants for the Avri integration.""" -CONF_COUNTRY_CODE = "country_code" -CONF_ZIP_CODE = "zip_code" -CONF_HOUSE_NUMBER = "house_number" -CONF_HOUSE_NUMBER_EXTENSION = "house_number_extension" -DOMAIN = "avri" -ICON = "mdi:trash-can-outline" -DEFAULT_COUNTRY_CODE = "NL" diff --git a/homeassistant/components/avri/manifest.json b/homeassistant/components/avri/manifest.json deleted file mode 100644 index 8a418bfb7b..0000000000 --- a/homeassistant/components/avri/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "avri", - "name": "Avri", - "documentation": "https://www.home-assistant.io/integrations/avri", - "requirements": [ - "avri-api==0.1.7", - "pycountry==19.8.18" - ], - "codeowners": [ - "@timvancann" - ], - "config_flow": true -} \ No newline at end of file diff --git a/homeassistant/components/avri/sensor.py b/homeassistant/components/avri/sensor.py deleted file mode 100644 index 06519a5c45..0000000000 --- a/homeassistant/components/avri/sensor.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Support for Avri waste curbside collection pickup.""" -import logging - -from avri.api import Avri, AvriException - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, DEVICE_CLASS_TIMESTAMP -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType - -from .const import DOMAIN, ICON - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities -) -> None: - """Set up the Avri Waste platform.""" - client = hass.data[DOMAIN][entry.entry_id] - integration_id = entry.data[CONF_ID] - - try: - each_upcoming = await hass.async_add_executor_job(client.upcoming_of_each) - except AvriException as ex: - raise PlatformNotReady from ex - else: - entities = [ - AvriWasteUpcoming(client, upcoming.name, integration_id) - for upcoming in each_upcoming - ] - async_add_entities(entities, True) - - -class AvriWasteUpcoming(Entity): - """Avri Waste Sensor.""" - - def __init__(self, client: Avri, waste_type: str, integration_id: str): - """Initialize the sensor.""" - self._waste_type = waste_type - self._name = f"{self._waste_type}".title() - self._state = None - self._client = client - self._state_available = False - self._integration_id = integration_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return (f"{self._integration_id}" f"-{self._waste_type}").replace(" ", "") - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return True if entity is available.""" - return self._state_available - - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON - - async def async_update(self): - """Update the data.""" - if not self.enabled: - return - - try: - pickup_events = self._client.upcoming_of_each() - except AvriException as ex: - _LOGGER.error( - "There was an error retrieving upcoming garbage pickups: %s", ex - ) - self._state_available = False - self._state = None - else: - self._state_available = True - matched_events = list( - filter(lambda event: event.name == self._waste_type, pickup_events) - ) - if not matched_events: - self._state = None - else: - self._state = matched_events[0].day.date() diff --git a/homeassistant/components/avri/strings.json b/homeassistant/components/avri/strings.json deleted file mode 100644 index e00409ffa2..0000000000 --- a/homeassistant/components/avri/strings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_house_number": "Invalid house number.", - "invalid_country_code": "Unknown 2 letter country code." - }, - "step": { - "user": { - "data": { - "zip_code": "Zip code", - "house_number": "House number", - "house_number_extension": "House number extension", - "country_code": "2 Letter country code" - }, - "description": "Enter your address", - "title": "Avri" - } - } - } -} diff --git a/homeassistant/components/avri/translations/ar.json b/homeassistant/components/avri/translations/ar.json deleted file mode 100644 index b23bf7e897..0000000000 --- a/homeassistant/components/avri/translations/ar.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u062a\u0645 \u062a\u0643\u0648\u064a\u0646 \u0647\u0630\u0627 \u0627\u0644\u0639\u0646\u0648\u0627\u0646 \u0628\u0627\u0644\u0641\u0639\u0644." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/ca.json b/homeassistant/components/avri/translations/ca.json deleted file mode 100644 index 77edbd4990..0000000000 --- a/homeassistant/components/avri/translations/ca.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" - }, - "error": { - "invalid_country_code": "Codi de pa\u00eds desconegut.", - "invalid_house_number": "N\u00famero de casa no v\u00e0lid." - }, - "step": { - "user": { - "data": { - "country_code": "Codi de pa\u00eds de 2 lletres", - "house_number": "N\u00famero de casa", - "house_number_extension": "Ampliaci\u00f3 de n\u00famero de casa", - "zip_code": "Codi postal" - }, - "description": "Introdueix la teva adre\u00e7a", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/cs.json b/homeassistant/components/avri/translations/cs.json deleted file mode 100644 index e46abc942c..0000000000 --- a/homeassistant/components/avri/translations/cs.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" - }, - "error": { - "invalid_country_code": "Nezn\u00e1m\u00fd dvoup\u00edsmenn\u00fd k\u00f3d zem\u011b.", - "invalid_house_number": "Neplatn\u00e9 \u010d\u00edslo domu." - }, - "step": { - "user": { - "data": { - "country_code": "2p\u00edsmenn\u00fd k\u00f3d zem\u011b", - "house_number": "\u010c\u00edslo domu", - "house_number_extension": "Roz\u0161\u00ed\u0159en\u00ed \u010d\u00edsla domu", - "zip_code": "PS\u010c" - }, - "description": "Zadejte svou adresu", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/de.json b/homeassistant/components/avri/translations/de.json deleted file mode 100644 index fc0ece086a..0000000000 --- a/homeassistant/components/avri/translations/de.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Position ist bereits konfiguriert" - }, - "error": { - "invalid_house_number": "Ung\u00fcltige Hausnummer" - }, - "step": { - "user": { - "data": { - "house_number": "Hausnummer", - "zip_code": "Postleitzahl" - }, - "description": "Gibt deine Adresse ein", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/en.json b/homeassistant/components/avri/translations/en.json deleted file mode 100644 index 832849a706..0000000000 --- a/homeassistant/components/avri/translations/en.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Location is already configured" - }, - "error": { - "invalid_country_code": "Unknown 2 letter country code.", - "invalid_house_number": "Invalid house number." - }, - "step": { - "user": { - "data": { - "country_code": "2 Letter country code", - "house_number": "House number", - "house_number_extension": "House number extension", - "zip_code": "Zip code" - }, - "description": "Enter your address", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/es.json b/homeassistant/components/avri/translations/es.json deleted file mode 100644 index 11539723fa..0000000000 --- a/homeassistant/components/avri/translations/es.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta direcci\u00f3n ya est\u00e1 configurada." - }, - "error": { - "invalid_country_code": "C\u00f3digo de pa\u00eds de 2 letras desconocido.", - "invalid_house_number": "N\u00famero de casa no v\u00e1lido." - }, - "step": { - "user": { - "data": { - "country_code": "C\u00f3digo de pa\u00eds de 2 letras", - "house_number": "N\u00famero de casa", - "house_number_extension": "Extensi\u00f3n del n\u00famero de casa", - "zip_code": "C\u00f3digo postal" - }, - "description": "Introduce tu direccion", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/et.json b/homeassistant/components/avri/translations/et.json deleted file mode 100644 index 0e83b89364..0000000000 --- a/homeassistant/components/avri/translations/et.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Aadress on juba m\u00e4\u00e4ratud" - }, - "error": { - "invalid_country_code": "Tundmatu kahet\u00e4heline riigikood.", - "invalid_house_number": "Tundmatu majanumber." - }, - "step": { - "user": { - "data": { - "country_code": "Kahet\u00e4heline riigikood", - "house_number": "Maja number", - "house_number_extension": "Maja numbri laiendus", - "zip_code": "Postiindeks" - }, - "description": "Sisesta oma aadress", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/fr.json b/homeassistant/components/avri/translations/fr.json deleted file mode 100644 index 188f82beae..0000000000 --- a/homeassistant/components/avri/translations/fr.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cette adresse est d\u00e9j\u00e0 configur\u00e9e." - }, - "error": { - "invalid_country_code": "Code pays \u00e0 2 lettres inconnu.", - "invalid_house_number": "Num\u00e9ro de maison invalide." - }, - "step": { - "user": { - "data": { - "country_code": "Code pays \u00e0 2 lettres", - "house_number": "Num\u00e9ro de maison", - "house_number_extension": "Extension de num\u00e9ro de maison", - "zip_code": "Code postal" - }, - "description": "Entrez votre adresse", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/it.json b/homeassistant/components/avri/translations/it.json deleted file mode 100644 index 50c92e0678..0000000000 --- a/homeassistant/components/avri/translations/it.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" - }, - "error": { - "invalid_country_code": "Codice paese di 2 lettere sconosciuto.", - "invalid_house_number": "Numero civico non valido." - }, - "step": { - "user": { - "data": { - "country_code": "Codice paese di 2 lettere", - "house_number": "Numero civico", - "house_number_extension": "Estensione del numero civico", - "zip_code": "Codice di avviamento postale" - }, - "description": "Inserisci il tuo indirizzo", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/ko.json b/homeassistant/components/avri/translations/ko.json deleted file mode 100644 index ab6504519d..0000000000 --- a/homeassistant/components/avri/translations/ko.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uc774 \uc8fc\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." - }, - "error": { - "invalid_country_code": "\uc54c \uc218 \uc5c6\ub294 \uad6d\uac00\ucf54\ub4dc\uc785\ub2c8\ub2e4.", - "invalid_house_number": "\uc9d1 \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "country_code": "2 \ubb38\uc790 \uad6d\uac00\ucf54\ub4dc", - "house_number": "\uc9d1 \ubc88\ud638", - "house_number_extension": "\uc9d1 \ubc88\ud638 \ucd94\uac00\uc815\ubcf4", - "zip_code": "\uc6b0\ud3b8 \ubc88\ud638" - }, - "description": "\uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/lb.json b/homeassistant/components/avri/translations/lb.json deleted file mode 100644 index 657640c2be..0000000000 --- a/homeassistant/components/avri/translations/lb.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standuert ass scho konfigur\u00e9iert." - }, - "error": { - "invalid_country_code": "Onbekannte Zweestellege L\u00e4nner Code", - "invalid_house_number": "Ong\u00eblteg Haus Nummer" - }, - "step": { - "user": { - "data": { - "country_code": "Zweestellege L\u00e4nner Code", - "house_number": "Haus Nummer", - "house_number_extension": "Haus Nummer Extensioun", - "zip_code": "Postleitzuel" - }, - "description": "G\u00ebff deng Adresse un", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/nl.json b/homeassistant/components/avri/translations/nl.json deleted file mode 100644 index a5be62bfc1..0000000000 --- a/homeassistant/components/avri/translations/nl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Locatie is al geconfigureerd" - }, - "error": { - "invalid_country_code": "Onbekende 2-letterige landcode." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/no.json b/homeassistant/components/avri/translations/no.json deleted file mode 100644 index 3f1edaf4c7..0000000000 --- a/homeassistant/components/avri/translations/no.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Plasseringen er allerede konfigurert" - }, - "error": { - "invalid_country_code": "Ukjent landskode p\u00e5 2 bokstaver.", - "invalid_house_number": "Ugyldig husnummer." - }, - "step": { - "user": { - "data": { - "country_code": "2 Bokstavs landskode", - "house_number": "Husnummer", - "house_number_extension": "Utvidelse av husnummer", - "zip_code": "Postnummer" - }, - "description": "Skriv inn adressen din", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/pl.json b/homeassistant/components/avri/translations/pl.json deleted file mode 100644 index dfe3f85a38..0000000000 --- a/homeassistant/components/avri/translations/pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" - }, - "error": { - "invalid_country_code": "Nieznany dwuliterowy kod kraju", - "invalid_house_number": "Nieprawid\u0142owy numer domu" - }, - "step": { - "user": { - "data": { - "country_code": "Dwuliterowy kod kraju", - "house_number": "Numer domu", - "house_number_extension": "Numer mieszkania", - "zip_code": "Kod pocztowy" - }, - "description": "Wpisz sw\u00f3j adres", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/pt.json b/homeassistant/components/avri/translations/pt.json deleted file mode 100644 index 77ddb25fb6..0000000000 --- a/homeassistant/components/avri/translations/pt.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" - }, - "step": { - "user": { - "data": { - "zip_code": "C\u00f3digo postal" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/ru.json b/homeassistant/components/avri/translations/ru.json deleted file mode 100644 index 01003d0e9d..0000000000 --- a/homeassistant/components/avri/translations/ru.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." - }, - "error": { - "invalid_country_code": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u0434\u0432\u0443\u0445\u0431\u0443\u043a\u0432\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b.", - "invalid_house_number": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430." - }, - "step": { - "user": { - "data": { - "country_code": "\u0414\u0432\u0443\u0445\u0431\u0443\u043a\u0432\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b", - "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430", - "house_number_extension": "\u041b\u0438\u0442\u0435\u0440 \u0434\u043e\u043c\u0430 / \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435", - "zip_code": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Avri.", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/zh-Hant.json b/homeassistant/components/avri/translations/zh-Hant.json deleted file mode 100644 index 566a9e43dc..0000000000 --- a/homeassistant/components/avri/translations/zh-Hant.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "invalid_country_code": "\u672a\u77e5\u570b\u78bc\uff08\u5169\u5b57\u6bcd\uff09\u3002", - "invalid_house_number": "\u9580\u724c\u865f\u78bc\u932f\u8aa4\u3002" - }, - "step": { - "user": { - "data": { - "country_code": "\u570b\u78bc\uff08\u5169\u5b57\u6bcd\uff09", - "house_number": "\u9580\u724c\u865f\u78bc", - "house_number_extension": "\u9580\u724c\u865f\u78bc\u5206\u865f", - "zip_code": "\u90f5\u905e\u5340\u865f" - }, - "description": "\u8f38\u5165\u5730\u5740", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 833f11190b..11a0b51764 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -23,7 +23,6 @@ FLOWS = [ "atag", "august", "aurora", - "avri", "awair", "axis", "azure_devops", diff --git a/requirements_all.txt b/requirements_all.txt index 99f972dc59..ec7b256775 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -305,9 +305,6 @@ av==8.0.2 # homeassistant.components.avion # avion==0.10 -# homeassistant.components.avri -avri-api==0.1.7 - # homeassistant.components.axis axis==41 @@ -1321,9 +1318,6 @@ pycomfoconnect==0.3 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 -# homeassistant.components.avri -pycountry==19.8.18 - # homeassistant.components.microsoft pycsspeechtts==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9385f6956d..6fc1b3da39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,9 +176,6 @@ auroranoaa==0.0.2 # homeassistant.components.stream av==8.0.2 -# homeassistant.components.avri -avri-api==0.1.7 - # homeassistant.components.axis axis==41 @@ -666,9 +663,6 @@ pychromecast==7.6.0 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 -# homeassistant.components.avri -pycountry==19.8.18 - # homeassistant.components.daikin pydaikin==2.4.0 diff --git a/tests/components/avri/__init__.py b/tests/components/avri/__init__.py deleted file mode 100644 index c521285503..0000000000 --- a/tests/components/avri/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Avri integration.""" diff --git a/tests/components/avri/test_config_flow.py b/tests/components/avri/test_config_flow.py deleted file mode 100644 index 2dba3c3c11..0000000000 --- a/tests/components/avri/test_config_flow.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test the Avri config flow.""" -from homeassistant import config_entries, setup -from homeassistant.components.avri.const import DOMAIN - -from tests.async_mock import patch - - -async def test_form(hass): - """Test we get the form.""" - await setup.async_setup_component(hass, "avri", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch( - "homeassistant.components.avri.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "zip_code": "1234AB", - "house_number": 42, - "house_number_extension": "", - "country_code": "NL", - }, - ) - - assert result2["type"] == "create_entry" - assert result2["title"] == "1234AB 42" - assert result2["data"] == { - "id": "1234AB 42", - "zip_code": "1234AB", - "house_number": 42, - "house_number_extension": "", - "country_code": "NL", - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_house_number(hass): - """Test we handle invalid house number.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "zip_code": "1234AB", - "house_number": -1, - "house_number_extension": "", - "country_code": "NL", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"house_number": "invalid_house_number"} - - -async def test_form_invalid_country_code(hass): - """Test we handle invalid county code.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "zip_code": "1234AB", - "house_number": 42, - "house_number_extension": "", - "country_code": "foo", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"country_code": "invalid_country_code"} From 9531b08f2a571716f375d26ad4579a46099003ab Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sun, 27 Dec 2020 09:39:36 +0100 Subject: [PATCH 230/302] Add explicit support for Luxembourg Smarty meter in dsmr integration (#43975) * Add support for Luxembourg Smarty meter * Add config flow test * Add sensor tests --- homeassistant/components/dsmr/config_flow.py | 10 ++- homeassistant/components/dsmr/sensor.py | 23 ++++++- tests/components/dsmr/conftest.py | 28 ++++++-- tests/components/dsmr/test_config_flow.py | 23 +++++++ tests/components/dsmr/test_sensor.py | 69 ++++++++++++++++++++ 5 files changed, 140 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 912deb7ffe..f089959835 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -35,11 +35,15 @@ class DSMRConnection: self._port = port self._dsmr_version = dsmr_version self._telegram = {} + if dsmr_version == "5L": + self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER + else: + self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER def equipment_identifier(self): """Equipment identifier.""" - if obis_ref.EQUIPMENT_IDENTIFIER in self._telegram: - dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER] + if self._equipment_identifier in self._telegram: + dsmr_object = self._telegram[self._equipment_identifier] return getattr(dsmr_object, "value", None) def equipment_identifier_gas(self): @@ -52,7 +56,7 @@ class DSMRConnection: """Test if we can validate connection with the device.""" def update_telegram(telegram): - if obis_ref.EQUIPMENT_IDENTIFIER in telegram: + if self._equipment_identifier in telegram: self._telegram = telegram transport.close() diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index cc1877fb5b..0c53fbd607 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -54,7 +54,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(["5B", "5", "4", "2.2"]) + cv.string, vol.In(["5L", "5B", "5", "4", "2.2"]) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), @@ -85,7 +85,6 @@ async def async_setup_entry( ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["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], @@ -112,6 +111,24 @@ async def async_setup_entry( ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], ] + if dsmr_version == "5L": + obis_mapping.extend( + [ + [ + "Energy Consumption (total)", + obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + ], + [ + "Energy Production (total)", + obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + ], + ] + ) + else: + obis_mapping.extend( + [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL]] + ) + # Generate device entities devices = [ DSMREntity(name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config) @@ -120,7 +137,7 @@ async def async_setup_entry( # Protocol version specific obis if CONF_SERIAL_ID_GAS in config: - if dsmr_version in ("4", "5"): + if dsmr_version in ("4", "5", "5L"): gas_obis = obis_ref.HOURLY_GAS_METER_READING elif dsmr_version in ("5B",): gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index d2cec93df9..d57828fdfa 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -2,7 +2,11 @@ import asyncio from dsmr_parser.clients.protocol import DSMRProtocol -from dsmr_parser.obis_references import EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS +from dsmr_parser.obis_references import ( + EQUIPMENT_IDENTIFIER, + EQUIPMENT_IDENTIFIER_GAS, + LUXEMBOURG_EQUIPMENT_IDENTIFIER, +) from dsmr_parser.objects import CosemObject import pytest @@ -38,17 +42,27 @@ async def dsmr_connection_send_validate_fixture(hass): transport = MagicMock(spec=asyncio.Transport) protocol = MagicMock(spec=DSMRProtocol) - async def connection_factory(*args, **kwargs): - """Return mocked out Asyncio classes.""" - return (transport, protocol) - - connection_factory = MagicMock(wraps=connection_factory) - protocol.telegram = { EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), } + async def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + if args[1] == "5L": + protocol.telegram = { + LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemObject( + [{"value": "12345678", "unit": ""}] + ), + EQUIPMENT_IDENTIFIER_GAS: CosemObject( + [{"value": "123456789", "unit": ""}] + ), + } + + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + async def wait_closed(): if isinstance(connection_factory.call_args_list[0][0][2], str): # TCP diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 039002ca7a..9ae49419bf 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -242,3 +242,26 @@ async def test_options_flow(hass): await hass.async_block_till_done() assert entry.options == {"time_between_update": 15} + + +async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5L", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/ttyUSB0" + assert result["data"] == {**entry_data, **SERIAL_DATA} diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ceccc7d8c3..76a9a5bb07 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -337,6 +337,75 @@ async def test_v5_meter(hass, dsmr_connection_fixture): assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS +async def test_luxembourg_meter(hass, dsmr_connection_fixture): + """Test if v5 meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + HOURLY_GAS_METER_READING, + LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5L", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + ] + ), + LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + ), + LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + 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) + + power_tariff = hass.states.get("sensor.energy_consumption_total") + assert power_tariff.state == "123.456" + assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + + power_tariff = hass.states.get("sensor.energy_production_total") + assert power_tariff.state == "654.321" + assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + + # 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") == VOLUME_CUBIC_METERS + + async def test_belgian_meter(hass, dsmr_connection_fixture): """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture From 51b88337caec91f86a6a6c9ace40fba62e271f56 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Dec 2020 00:49:22 -0800 Subject: [PATCH 231/302] Simplify nest event handling (#44367) * Simplify nest event handling Use device specific update callbacks rather than a global callback. The motivation is to prepare for a follow up change that will store camera specific event tokens on the camera itself, so that a service can later fetch event specific image snapshots, which would be difficult to send across the event bus. * Increase nest camera test coverage * Remove unnecessary device updates for nest cameras * Remove unused imports * Fix device id check to look at returned entry * Remove unused imports after rebase * Partial revert of nest event simplification * Push more update logic into the nest library * Revert nest device_info changes * Revert test changes to restore global update behavior * Bump nest library version to support new callback interfaces --- homeassistant/components/nest/__init__.py | 19 ++++--------------- homeassistant/components/nest/camera_sdm.py | 10 ++-------- homeassistant/components/nest/climate_sdm.py | 12 ++---------- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/sensor_sdm.py | 12 ++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 8 ++++---- tests/components/nest/test_device_trigger.py | 3 ++- 9 files changed, 19 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index e9bffa2706..ab235d7559 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -3,7 +3,7 @@ import asyncio import logging -from google_nest_sdm.event import AsyncEventCallback, EventMessage +from google_nest_sdm.event import EventMessage from google_nest_sdm.exceptions import AuthException, GoogleNestException from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol @@ -24,7 +24,6 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send from . import api, config_flow from .const import ( @@ -34,7 +33,6 @@ from .const import ( DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, - SIGNAL_NEST_UPDATE, ) from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry @@ -106,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -class SignalUpdateCallback(AsyncEventCallback): +class SignalUpdateCallback: """An EventCallback invoked when new events arrive from subscriber.""" def __init__(self, hass: HomeAssistant): @@ -116,17 +114,8 @@ class SignalUpdateCallback(AsyncEventCallback): async def async_handle_event(self, event_message: EventMessage): """Process an incoming EventMessage.""" if not event_message.resource_update_name: - _LOGGER.debug("Ignoring event with no device_id") return device_id = event_message.resource_update_name - _LOGGER.debug("Update for %s @ %s", device_id, event_message.timestamp) - traits = event_message.resource_update_traits - if traits: - _LOGGER.debug("Trait update %s", traits.keys()) - # This event triggered an update to a device that changed some - # properties which the DeviceManager should already have received. - # Send a signal to refresh state of all listening devices. - async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE) events = event_message.resource_update_events if not events: return @@ -134,7 +123,6 @@ class SignalUpdateCallback(AsyncEventCallback): device_registry = await self._hass.helpers.device_registry.async_get_registry() device_entry = device_registry.async_get_device({(DOMAIN, device_id)}, ()) if not device_entry: - _LOGGER.debug("Ignoring event for unregistered device '%s'", device_id) return for event in events: event_type = EVENT_NAME_MAP.get(event) @@ -170,7 +158,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): subscriber = GoogleNestSubscriber( auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] ) - subscriber.set_update_callback(SignalUpdateCallback(hass)) + callback = SignalUpdateCallback(hass) + subscriber.set_update_callback(callback.async_handle_event) try: await subscriber.start_async() diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 37bd2fed8a..a643de0e6c 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -13,12 +13,11 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow -from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -151,13 +150,8 @@ class NestCamera(Camera): async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted - # here to re-fresh the signals from _device. Unregister this callback - # when the entity is removed. self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_NEST_UPDATE, self.async_write_ha_state - ) + self._device.add_update_listener(self.async_write_ha_state) ) async def async_camera_image(self): diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 368eb8b346..08cb0161bd 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -36,10 +36,9 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo # Mapping for sdm.devices.traits.ThermostatMode mode field @@ -126,16 +125,9 @@ class ThermostatEntity(ClimateEntity): async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted - # here to re-fresh the signals from _device. Unregister this callback - # when the entity is removed. self._supported_features = self._get_supported_features() self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_NEST_UPDATE, - self.async_write_ha_state, - ) + self._device.add_update_listener(self.async_write_ha_state) ) @property diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 028f56587a..7d60bb1cf5 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0", - "google-nest-sdm==0.2.1" + "google-nest-sdm==0.2.5" ], "codeowners": [ "@awarecan", diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 9009414c5b..52490f41f8 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -15,11 +15,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -80,15 +79,8 @@ class SensorBase(Entity): async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted - # here to re-fresh the signals from _device. Unregister this callback - # when the entity is removed. self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_NEST_UPDATE, - self.async_write_ha_state, - ) + self._device.add_update_listener(self.async_write_ha_state) ) diff --git a/requirements_all.txt b/requirements_all.txt index ec7b256775..917006cea9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.1 +google-nest-sdm==0.2.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fc1b3da39..485caf8bfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.1 +google-nest-sdm==0.2.5 # homeassistant.components.gree greeclimate==0.10.3 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index cd3a06a5af..d7b78b98f8 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -1,9 +1,10 @@ """Common libraries for test setup.""" import time +from typing import Awaitable, Callable from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.event import AsyncEventCallback, EventMessage +from google_nest_sdm.event import EventMessage from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN @@ -59,9 +60,8 @@ class FakeSubscriber(GoogleNestSubscriber): def __init__(self, device_manager: FakeDeviceManager): """Initialize Fake Subscriber.""" self._device_manager = device_manager - self._callback = None - def set_update_callback(self, callback: AsyncEventCallback): + def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" self._callback = callback @@ -81,7 +81,7 @@ class FakeSubscriber(GoogleNestSubscriber): """Simulate a received pubsub message, invoked by tests.""" # Update device state, then invoke HomeAssistant to refresh await self._device_manager.async_handle_event(event_message) - await self._callback.async_handle_event(event_message) + await self._callback(event_message) async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 3199b89f21..29091f32f5 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -7,7 +7,8 @@ import homeassistant.components.automation as automation from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.components.nest import DOMAIN, NEST_EVENT +from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.events import NEST_EVENT from homeassistant.setup import async_setup_component from .common import async_setup_sdm_platform From 6d043f2ca12ca6e218c86e168f40da4fe5348fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 27 Dec 2020 16:36:35 +0100 Subject: [PATCH 232/302] Tado: add full list of devices (#44475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/tado/__init__.py | 17 +-- .../components/tado/binary_sensor.py | 127 ++++++++++++++++++ homeassistant/components/tado/climate.py | 10 +- homeassistant/components/tado/const.py | 5 +- homeassistant/components/tado/entity.py | 45 +++++-- homeassistant/components/tado/sensor.py | 102 +------------- homeassistant/components/tado/water_heater.py | 4 +- tests/components/tado/test_binary_sensor.py | 14 ++ tests/components/tado/test_sensor.py | 9 -- 9 files changed, 191 insertions(+), 142 deletions(-) create mode 100644 homeassistant/components/tado/binary_sensor.py create mode 100644 tests/components/tado/test_binary_sensor.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 44a0f551ae..ab64183a14 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -30,7 +30,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TADO_COMPONENTS = ["sensor", "climate", "water_heater"] +TADO_COMPONENTS = ["binary_sensor", "sensor", "climate", "water_heater"] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=15) @@ -174,7 +174,6 @@ class TadoConnector: self.devices = None self.data = { "zone": {}, - "device": {}, } @property @@ -188,16 +187,15 @@ class TadoConnector: self.tado.setDebugging(True) # Load zones and devices self.zones = self.tado.getZones() - self.devices = self.tado.getMe()["homes"] - self.device_id = self.devices[0]["id"] + self.devices = self.tado.getDevices() + self.device_id = self.tado.getMe()["homes"][0]["id"] @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the registered zones.""" for zone in self.zones: self.update_sensor("zone", zone["id"]) - for device in self.devices: - self.update_sensor("device", device["id"]) + self.devices = self.tado.getDevices() def update_sensor(self, sensor_type, sensor): """Update the internal data from Tado.""" @@ -205,13 +203,6 @@ class TadoConnector: try: if sensor_type == "zone": data = self.tado.getZoneState(sensor) - elif sensor_type == "device": - devices_data = self.tado.getDevices() - if not devices_data: - _LOGGER.info("There are no devices to setup on this tado account") - return - - data = devices_data[0] else: _LOGGER.debug("Unknown sensor: %s", sensor_type) return diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py new file mode 100644 index 0000000000..852ff6ae54 --- /dev/null +++ b/homeassistant/components/tado/binary_sensor.py @@ -0,0 +1,127 @@ +"""Support for Tado sensors for each zone.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_BATTERY, TYPE_POWER +from .entity import TadoDeviceEntity + +_LOGGER = logging.getLogger(__name__) + +DEVICE_SENSORS = { + TYPE_BATTERY: [ + "battery state", + "connection state", + ], + TYPE_POWER: [ + "connection state", + ], +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up the Tado sensor platform.""" + + tado = hass.data[DOMAIN][entry.entry_id][DATA] + devices = tado.devices + entities = [] + + # Create device sensors + for device in devices: + if "batteryState" in device: + device_type = TYPE_BATTERY + else: + device_type = TYPE_POWER + + entities.extend( + [ + TadoDeviceSensor(tado, device, variable) + for variable in DEVICE_SENSORS[device_type] + ] + ) + + if entities: + async_add_entities(entities, True) + + +class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): + """Representation of a tado Sensor.""" + + def __init__(self, tado, device_info, device_variable): + """Initialize of the Tado Sensor.""" + self._tado = tado + super().__init__(device_info) + + self.device_variable = device_variable + + self._unique_id = f"{device_variable} {self.device_id} {tado.device_id}" + + self._state = None + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format( + self._tado.device_id, "device", self.device_id + ), + self._async_update_callback, + ) + ) + self._async_update_device_data() + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.device_name} {self.device_variable}" + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this sensor.""" + if self.device_variable == "battery state": + return DEVICE_CLASS_BATTERY + if self.device_variable == "connection state": + return DEVICE_CLASS_CONNECTIVITY + return None + + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_device_data() + self.async_write_ha_state() + + @callback + def _async_update_device_data(self): + """Handle update callbacks.""" + for device in self._tado.devices: + if device["serialNo"] == self.device_id: + self._device_info = device + break + + if self.device_variable == "battery state": + self._state = self._device_info["batteryState"] == "LOW" + elif self.device_variable == "connection state": + self._state = self._device_info.get("connectionState", {}).get( + "value", False + ) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 3e0c79ad65..438b7a2139 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -89,15 +89,13 @@ def _generate_entities(tado): entities = [] for zone in tado.zones: if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]: - entity = create_climate_entity( - tado, zone["name"], zone["id"], zone["devices"][0] - ) + entity = create_climate_entity(tado, zone["name"], zone["id"]) if entity: entities.append(entity) return entities -def create_climate_entity(tado, name: str, zone_id: int, zone: dict): +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) @@ -180,7 +178,6 @@ def create_climate_entity(tado, name: str, zone_id: int, zone: dict): supported_hvac_modes, supported_fan_modes, support_flags, - zone, ) return entity @@ -203,11 +200,10 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): supported_hvac_modes, supported_fan_modes, support_flags, - device_info, ): """Initialize of Tado climate entity.""" self._tado = tado - super().__init__(zone_name, device_info, tado.device_id, zone_id) + super().__init__(zone_name, tado.device_id, zone_id) self.zone_id = zone_id self.zone_type = zone_type diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 9fc7b19805..95c524b043 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -53,6 +53,9 @@ TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" TYPE_HEATING = "HEATING" TYPE_HOT_WATER = "HOT_WATER" +TYPE_BATTERY = "BATTERY" +TYPE_POWER = "POWER" + # Base modes CONST_MODE_OFF = "OFF" CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule @@ -144,6 +147,6 @@ UNIQUE_ID = "unique_id" DEFAULT_NAME = "Tado" -TADO_BRIDGE = "Tado Bridge" +TADO_ZONE = "Zone" UPDATE_LISTENER = "update_listener" diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index d91896a4e1..fa2ce2f3af 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -1,25 +1,25 @@ -"""Base class for August entity.""" +"""Base class for Tado entity.""" from homeassistant.helpers.entity import Entity -from .const import DEFAULT_NAME, DOMAIN +from .const import DEFAULT_NAME, DOMAIN, TADO_ZONE -class TadoZoneEntity(Entity): - """Base implementation for tado device.""" +class TadoDeviceEntity(Entity): + """Base implementation for Tado device.""" - def __init__(self, zone_name, device_info, device_id, zone_id): - """Initialize an August device.""" + def __init__(self, device_info): + """Initialize a Tado device.""" super().__init__() - self._device_zone_id = f"{device_id}_{zone_id}" self._device_info = device_info - self.zone_name = zone_name + self.device_name = device_info["shortSerialNo"] + self.device_id = device_info["serialNo"] @property def device_info(self): """Return the device_info of the device.""" return { - "identifiers": {(DOMAIN, self._device_zone_id)}, - "name": self.zone_name, + "identifiers": {(DOMAIN, self.device_id)}, + "name": self.device_name, "manufacturer": DEFAULT_NAME, "sw_version": self._device_info["currentFwVersion"], "model": self._device_info["deviceType"], @@ -30,3 +30,28 @@ class TadoZoneEntity(Entity): def should_poll(self): """Do not poll.""" return False + + +class TadoZoneEntity(Entity): + """Base implementation for Tado zone.""" + + def __init__(self, zone_name, device_id, zone_id): + """Initialize a Tado zone.""" + super().__init__() + self._device_zone_id = f"{device_id}_{zone_id}" + self.zone_name = zone_name + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._device_zone_id)}, + "name": self.zone_name, + "manufacturer": DEFAULT_NAME, + "model": TADO_ZONE, + } + + @property + def should_poll(self): + """Do not poll.""" + return False diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 56be5eb012..a937aea25e 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -9,10 +9,8 @@ from homeassistant.helpers.entity import Entity from .const import ( DATA, - DEFAULT_NAME, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, - TADO_BRIDGE, TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER, @@ -46,8 +44,6 @@ ZONE_SENSORS = { TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], } -DEVICE_SENSORS = ["tado bridge status"] - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -57,7 +53,6 @@ async def async_setup_entry( tado = hass.data[DOMAIN][entry.entry_id][DATA] # Create zone sensors zones = tado.zones - devices = tado.devices entities = [] for zone in zones: @@ -68,22 +63,11 @@ async def async_setup_entry( entities.extend( [ - TadoZoneSensor( - tado, zone["name"], zone["id"], variable, zone["devices"][0] - ) + TadoZoneSensor(tado, zone["name"], zone["id"], variable) for variable in ZONE_SENSORS[zone_type] ] ) - # Create device sensors - for device in devices: - entities.extend( - [ - TadoDeviceSensor(tado, device["name"], device["id"], variable, device) - for variable in DEVICE_SENSORS - ] - ) - if entities: async_add_entities(entities, True) @@ -91,10 +75,10 @@ async def async_setup_entry( class TadoZoneSensor(TadoZoneEntity, Entity): """Representation of a tado Sensor.""" - def __init__(self, tado, zone_name, zone_id, zone_variable, device_info): + def __init__(self, tado, zone_name, zone_id, zone_variable): """Initialize of the Tado Sensor.""" self._tado = tado - super().__init__(zone_name, device_info, tado.device_id, zone_id) + super().__init__(zone_name, tado.device_id, zone_id) self.zone_id = zone_id self.zone_variable = zone_variable @@ -227,83 +211,3 @@ class TadoZoneSensor(TadoZoneEntity, Entity): or self._tado_zone_data.open_window_detected ) self._state_attributes = self._tado_zone_data.open_window_attr - - -class TadoDeviceSensor(Entity): - """Representation of a tado Sensor.""" - - def __init__(self, tado, device_name, device_id, device_variable, device_info): - """Initialize of the Tado Sensor.""" - self._tado = tado - - self._device_info = device_info - self.device_name = device_name - self.device_id = device_id - self.device_variable = device_variable - - self._unique_id = f"{device_variable} {device_id} {tado.device_id}" - - self._state = None - self._state_attributes = None - self._tado_device_data = None - - async def async_added_to_hass(self): - """Register for sensor updates.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "device", self.device_id - ), - self._async_update_callback, - ) - ) - self._async_update_device_data() - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.device_name} {self.device_variable}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self): - """Do not poll.""" - return False - - @callback - def _async_update_callback(self): - """Update and write state.""" - self._async_update_device_data() - self.async_write_ha_state() - - @callback - def _async_update_device_data(self): - """Handle update callbacks.""" - try: - data = self._tado.data["device"][self.device_id] - except KeyError: - return - - if self.device_variable == "tado bridge status": - self._state = data.get("connectionState", {}).get("value", False) - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self.device_name, - "manufacturer": DEFAULT_NAME, - "model": TADO_BRIDGE, - } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 1a99db5c24..2755d14a3a 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -113,7 +113,6 @@ def create_water_heater_entity(tado, name: str, zone_id: int, zone: str): supports_temperature_control, min_temp, max_temp, - zone["devices"][0], ) return entity @@ -130,12 +129,11 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): supports_temperature_control, min_temp, max_temp, - device_info, ): """Initialize of Tado water heater entity.""" self._tado = tado - super().__init__(zone_name, device_info, tado.device_id, zone_id) + super().__init__(zone_name, tado.device_id, zone_id) self.zone_id = zone_id self._unique_id = f"{zone_id} {tado.device_id}" diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py new file mode 100644 index 0000000000..39dd068f5a --- /dev/null +++ b/tests/components/tado/test_binary_sensor.py @@ -0,0 +1,14 @@ +"""The sensor tests for the tado platform.""" + +from homeassistant.const import STATE_ON + +from .util import async_init_integration + + +async def test_home_create_binary_sensors(hass): + """Test creation of home binary sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.wr1_connection_state") + assert state.state == STATE_ON diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 2ea2c0508e..646e774153 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -85,12 +85,3 @@ async def test_water_heater_create_sensors(hass): state = hass.states.get("sensor.water_heater_power") assert state.state == "ON" - - -async def test_home_create_sensors(hass): - """Test creation of home sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.home_name_tado_bridge_status") - assert state.state == "True" From 71af0fac1644821b9303815b680914b9eeca444d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Dec 2020 20:30:51 -0800 Subject: [PATCH 233/302] Improve nest setup error handling (#44385) * Improve error handling user experience This is meant to make the nest integration quieter. Exceptions are handled with a single log error message. Co-authored-by: j-stienstra <65826735+j-stienstra@users.noreply.github.com> --- homeassistant/components/nest/__init__.py | 23 ++++- tests/components/nest/common.py | 4 +- tests/components/nest/test_config_flow_sdm.py | 2 +- tests/components/nest/test_init_sdm.py | 90 +++++++++++++++++++ 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 tests/components/nest/test_init_sdm.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index ab235d7559..e4be96cbf1 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -4,7 +4,11 @@ import asyncio import logging from google_nest_sdm.event import EventMessage -from google_nest_sdm.exceptions import AuthException, GoogleNestException +from google_nest_sdm.exceptions import ( + AuthException, + ConfigurationException, + GoogleNestException, +) from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol @@ -43,6 +47,9 @@ _LOGGER = logging.getLogger(__name__) CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" DATA_NEST_CONFIG = "nest_config" +DATA_NEST_UNAVAILABLE = "nest_unavailable" + +NEST_SETUP_NOTIFICATION = "nest_setup" SENSOR_SCHEMA = vol.Schema( {vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)} @@ -173,18 +180,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) return False + except ConfigurationException as err: + _LOGGER.error("Configuration error: %s", err) + subscriber.stop_async() + return False except GoogleNestException as err: - _LOGGER.error("Subscriber error: %s", err) + if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: + _LOGGER.error("Subscriber error: %s", err) + hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() raise ConfigEntryNotReady from err try: await subscriber.async_get_device_manager() except GoogleNestException as err: - _LOGGER.error("Device Manager error: %s", err) + if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: + _LOGGER.error("Device manager error: %s", err) + hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() raise ConfigEntryNotReady from err + hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber for component in PLATFORMS: @@ -213,5 +229,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) if unload_ok: hass.data[DOMAIN].pop(DATA_SUBSCRIBER) + hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) return unload_ok diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index d7b78b98f8..65a3756391 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -19,7 +19,7 @@ CONFIG = { "client_secret": "some-client-secret", # Required fields for using SDM API "project_id": "some-project-id", - "subscriber_id": "some-subscriber-id", + "subscriber_id": "projects/example/subscriptions/subscriber-id-9876", }, } @@ -65,7 +65,7 @@ class FakeSubscriber(GoogleNestSubscriber): """Capture the callback set by Home Assistant.""" self._callback = callback - async def start_async(self) -> DeviceManager: + async def start_async(self): """Return the fake device manager.""" return self._device_manager diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index e506f269d6..aad5621935 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -14,7 +14,7 @@ from tests.async_mock import patch CLIENT_ID = "1234" CLIENT_SECRET = "5678" PROJECT_ID = "project-id-4321" -SUBSCRIBER_ID = "subscriber-id-9876" +SUBSCRIBER_ID = "projects/example/subscriptions/subscriber-id-9876" CONFIG = { DOMAIN: { diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py new file mode 100644 index 0000000000..cb17f81d18 --- /dev/null +++ b/tests/components/nest/test_init_sdm.py @@ -0,0 +1,90 @@ +""" +Test for setup methods for the SDM API. + +The tests fake out the subscriber/devicemanager and simulate setup behavior +and failure modes. +""" + +import logging + +from google_nest_sdm.exceptions import GoogleNestException + +from homeassistant.components.nest import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.setup import async_setup_component + +from .common import CONFIG, CONFIG_ENTRY_DATA, async_setup_sdm_platform + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +PLATFORM = "sensor" + + +async def test_setup_success(hass, caplog): + """Test successful setup.""" + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm_platform(hass, PLATFORM) + assert not caplog.records + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_LOADED + + +async def async_setup_sdm(hass, config=CONFIG): + """Prepare test setup.""" + MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA).add_to_hass(hass) + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" + ): + await async_setup_component(hass, DOMAIN, config) + + +async def test_setup_configuration_failure(hass, caplog): + """Test configuration error.""" + config = CONFIG.copy() + config[DOMAIN]["subscriber_id"] = "invalid-subscriber-format" + + await async_setup_sdm(hass, config) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_ERROR + + # This error comes from the python google-nest-sdm library, as a check added + # to prevent common misconfigurations (e.g. confusing topic and subscriber) + assert "Subscription misconfigured. Expected subscriber_id" in caplog.text + + +async def test_setup_susbcriber_failure(hass, caplog): + """Test configuration error.""" + with patch( + "homeassistant.components.nest.GoogleNestSubscriber.start_async", + side_effect=GoogleNestException(), + ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm(hass) + assert "Subscriber error:" in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_RETRY + + +async def test_setup_device_manager_failure(hass, caplog): + """Test configuration error.""" + with patch("homeassistant.components.nest.GoogleNestSubscriber.start_async"), patch( + "homeassistant.components.nest.GoogleNestSubscriber.async_get_device_manager", + side_effect=GoogleNestException(), + ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm(hass) + assert len(caplog.messages) == 1 + assert "Device manager error:" in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_RETRY From ed576edde73dba3a5444f38a6b6b15017c9766fb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Dec 2020 14:41:39 +0100 Subject: [PATCH 234/302] Fix Tasmota device triggers (#44574) --- .../components/tasmota/device_trigger.py | 4 +- .../components/tasmota/test_device_trigger.py | 147 ++++++++++++++++-- 2 files changed, 139 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index e7dad0885a..f06d815e5c 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -56,7 +56,7 @@ class TriggerInstance: event_trigger.CONF_EVENT_TYPE: TASMOTA_EVENT, event_trigger.CONF_EVENT_DATA: { "mac": self.trigger.tasmota_trigger.cfg.mac, - "source": self.trigger.tasmota_trigger.cfg.source, + "source": self.trigger.tasmota_trigger.cfg.subtype, "event": self.trigger.tasmota_trigger.cfg.event, }, } @@ -126,7 +126,7 @@ class Trigger: def _on_trigger(): data = { "mac": self.tasmota_trigger.cfg.mac, - "source": self.tasmota_trigger.cfg.source, + "source": self.tasmota_trigger.cfg.subtype, "event": self.tasmota_trigger.cfg.event, } self.hass.bus.async_fire( diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 42fc5dc7a4..2c88533f30 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -21,7 +21,42 @@ from tests.common import ( from tests.components.blueprint.conftest import stub_blueprint_populate # noqa -async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): +async def test_get_triggers_btn(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): + """Test we get the expected triggers from a discovered mqtt device.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["btn"][0] = 1 + config["btn"][1] = 1 + config["so"]["13"] = 1 + config["so"]["73"] = 1 + mac = config["mac"] + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_1_SINGLE", + "type": "button_short_press", + "subtype": "button_1", + }, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_2_SINGLE", + "type": "button_short_press", + "subtype": "button_2", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_swc(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): """Test we get the expected triggers from a discovered mqtt device.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 @@ -239,13 +274,83 @@ async def test_update_remove_triggers( assert triggers == [] -async def test_if_fires_on_mqtt_message( +async def test_if_fires_on_mqtt_message_btn( hass, device_reg, calls, mqtt_mock, setup_tasmota ): - """Test triggers firing.""" + """Test button triggers firing.""" + # Discover a device with 2 device triggers + config = copy.deepcopy(DEFAULT_CONFIG) + config["btn"][0] = 1 + config["btn"][2] = 1 + config["so"]["73"] = 1 + mac = config["mac"] + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_1_SINGLE", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_1")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_3_SINGLE", + "subtype": "button_3", + "type": "button_short_press", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_3")}, + }, + }, + ] + }, + ) + + # Fake button 1 single press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Button1":{"Action":"SINGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "short_press_1" + + # Fake button 3 single press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Button3":{"Action":"SINGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "short_press_3" + + +async def test_if_fires_on_mqtt_message_swc( + hass, device_reg, calls, mqtt_mock, setup_tasmota +): + """Test switch triggers firing.""" # Discover a device with 2 device triggers config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 + config["swc"][1] = 0 config["swc"][2] = 9 config["swn"][2] = "custom_switch" mac = config["mac"] @@ -270,7 +375,21 @@ async def test_if_fires_on_mqtt_message( }, "action": { "service": "test.automation", - "data_template": {"some": ("short_press")}, + "data_template": {"some": ("short_press_1")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_switch_2_TOGGLE", + "type": "button_short_press", + "subtype": "switch_2", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_2")}, }, }, { @@ -284,28 +403,36 @@ async def test_if_fires_on_mqtt_message( }, "action": { "service": "test.automation", - "data_template": {"some": ("long_press")}, + "data_template": {"some": ("long_press_3")}, }, }, ] }, ) - # Fake short press. + # Fake switch 1 short press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert calls[0].data["some"] == "short_press_1" - # Fake long press. + # Fake switch 2 short press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Switch2":{"Action":"TOGGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "short_press_2" + + # Fake switch 3 long press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"custom_switch":{"Action":"HOLD"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "long_press" + assert len(calls) == 3 + assert calls[2].data["some"] == "long_press_3" async def test_if_fires_on_mqtt_message_late_discover( From 50e11773eeb9e1c8b803c3515a27db705eefb23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 28 Dec 2020 14:57:51 +0100 Subject: [PATCH 235/302] Tado: use proper variable name to avoid confusion (#44571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current device_id variable refers to the Home ID obtained from the Tado API. Let's use home_id in order to avoid confusion with Tado devices. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/tado/__init__.py | 8 ++++---- homeassistant/components/tado/binary_sensor.py | 4 ++-- homeassistant/components/tado/climate.py | 6 +++--- homeassistant/components/tado/entity.py | 4 ++-- homeassistant/components/tado/sensor.py | 6 +++--- homeassistant/components/tado/water_heater.py | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index ab64183a14..228ac48bcb 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -168,7 +168,7 @@ class TadoConnector: self._password = password self._fallback = fallback - self.device_id = None + self.home_id = None self.tado = None self.zones = None self.devices = None @@ -188,7 +188,7 @@ class TadoConnector: # Load zones and devices self.zones = self.tado.getZones() self.devices = self.tado.getDevices() - self.device_id = self.tado.getMe()["homes"][0]["id"] + self.home_id = self.tado.getMe()["homes"][0]["id"] @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -218,14 +218,14 @@ class TadoConnector: _LOGGER.debug( "Dispatching update to %s %s %s: %s", - self.device_id, + self.home_id, sensor_type, sensor, data, ) dispatcher_send( self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.device_id, sensor_type, sensor), + SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, sensor_type, sensor), ) def get_capabilities(self, zone_id): diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 852ff6ae54..279633b07b 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -63,7 +63,7 @@ class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): self.device_variable = device_variable - self._unique_id = f"{device_variable} {self.device_id} {tado.device_id}" + self._unique_id = f"{device_variable} {self.device_id} {tado.home_id}" self._state = None @@ -74,7 +74,7 @@ class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "device", self.device_id + self._tado.home_id, "device", self.device_id ), self._async_update_callback, ) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 438b7a2139..423205f15b 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -203,11 +203,11 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): ): """Initialize of Tado climate entity.""" self._tado = tado - super().__init__(zone_name, tado.device_id, zone_id) + super().__init__(zone_name, tado.home_id, zone_id) self.zone_id = zone_id self.zone_type = zone_type - self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" + self._unique_id = f"{zone_type} {zone_id} {tado.home_id}" self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._supported_hvac_modes = supported_hvac_modes @@ -245,7 +245,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "zone", self.zone_id + self._tado.home_id, "zone", self.zone_id ), self._async_update_callback, ) diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index fa2ce2f3af..03900fdeeb 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -35,10 +35,10 @@ class TadoDeviceEntity(Entity): class TadoZoneEntity(Entity): """Base implementation for Tado zone.""" - def __init__(self, zone_name, device_id, zone_id): + def __init__(self, zone_name, home_id, zone_id): """Initialize a Tado zone.""" super().__init__() - self._device_zone_id = f"{device_id}_{zone_id}" + self._device_zone_id = f"{home_id}_{zone_id}" self.zone_name = zone_name @property diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index a937aea25e..4e8f69b17c 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -78,12 +78,12 @@ class TadoZoneSensor(TadoZoneEntity, Entity): def __init__(self, tado, zone_name, zone_id, zone_variable): """Initialize of the Tado Sensor.""" self._tado = tado - super().__init__(zone_name, tado.device_id, zone_id) + super().__init__(zone_name, tado.home_id, zone_id) self.zone_id = zone_id self.zone_variable = zone_variable - self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}" + self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" self._state = None self._state_attributes = None @@ -96,7 +96,7 @@ class TadoZoneSensor(TadoZoneEntity, Entity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "zone", self.zone_id + self._tado.home_id, "zone", self.zone_id ), self._async_update_callback, ) diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 2755d14a3a..3fcdb6426f 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -133,10 +133,10 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Initialize of Tado water heater entity.""" self._tado = tado - super().__init__(zone_name, tado.device_id, zone_id) + super().__init__(zone_name, tado.home_id, zone_id) self.zone_id = zone_id - self._unique_id = f"{zone_id} {tado.device_id}" + self._unique_id = f"{zone_id} {tado.home_id}" self._device_is_active = False @@ -161,7 +161,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "zone", self.zone_id + self._tado.home_id, "zone", self.zone_id ), self._async_update_callback, ) From d95696e4f5fbf5c6428b49fb8645c4990885049e Mon Sep 17 00:00:00 2001 From: badguy99 <61918526+badguy99@users.noreply.github.com> Date: Mon, 28 Dec 2020 16:10:49 +0000 Subject: [PATCH 236/302] Soma cover battery level attribute (#44459) --- .coveragerc | 3 +- homeassistant/components/soma/__init__.py | 24 +++++++++++++- homeassistant/components/soma/sensor.py | 40 +++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/soma/sensor.py diff --git a/.coveragerc b/.coveragerc index 64a22ef275..c16b6ecb98 100644 --- a/.coveragerc +++ b/.coveragerc @@ -828,8 +828,9 @@ omit = homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/* homeassistant/components/solax/sensor.py - homeassistant/components/soma/cover.py homeassistant/components/soma/__init__.py + homeassistant/components/soma/cover.py + homeassistant/components/soma/sensor.py homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonos/* diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 93ee4fc9b8..3439684f97 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMA_COMPONENTS = ["cover"] +SOMA_COMPONENTS = ["cover", "sensor"] async def async_setup(hass, config): @@ -74,6 +74,7 @@ class SomaEntity(Entity): self.device = device self.api = api self.current_position = 50 + self.battery_state = 0 self.is_available = True @property @@ -120,4 +121,25 @@ class SomaEntity(Entity): self.is_available = False return self.current_position = 100 - response["position"] + try: + response = await self.hass.async_add_executor_job( + self.api.get_battery_level, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + self.is_available = False + return + # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API + # battery_level response is expected to be min = 360, max 410 for + # 0-100% levels above 410 are consider 100% and below 360, 0% as the + # device considers 360 the minimum to move the motor. + _battery = round(2 * (response["battery_level"] - 360)) + battery = max(min(100, _battery), 0) + self.battery_state = battery self.is_available = True diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py new file mode 100644 index 0000000000..2d37a0b0dc --- /dev/null +++ b/homeassistant/components/soma/sensor.py @@ -0,0 +1,40 @@ +"""Support for Soma sensors.""" +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.helpers.entity import Entity + +from . import DEVICES, SomaEntity +from .const import API, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Soma sensor platform.""" + + devices = hass.data[DOMAIN][DEVICES] + + async_add_entities( + [SomaSensor(sensor, hass.data[DOMAIN][API]) for sensor in devices], True + ) + + +class SomaSensor(SomaEntity, Entity): + """Representation of a Soma cover device.""" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_BATTERY + + @property + def name(self): + """Return the name of the device.""" + return self.device["name"] + " battery level" + + @property + def state(self): + """Return the state of the entity.""" + return self.battery_state + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return PERCENTAGE From 13d6f5454d0dca159886139162e5b4e7bdddf97b Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Mon, 28 Dec 2020 17:12:49 +0000 Subject: [PATCH 237/302] Turn on denonavr receiver when a source is changed (#44473) --- homeassistant/components/denonavr/media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index b5990dede2..73fe0f2152 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -338,6 +338,9 @@ class DenonDevice(MediaPlayerEntity): def select_source(self, source): """Select input source.""" + # Ensure that the AVR is turned on, which is necessary for input + # switch to work. + self.turn_on() return self._receiver.set_input_func(source) def select_sound_mode(self, sound_mode): From 392c058d34631eeffabaf48f6e80d16e7eb98ae4 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 28 Dec 2020 18:34:08 +0100 Subject: [PATCH 238/302] Ensure consistent spelling of "ID" (#44585) --- homeassistant/components/discord/notify.py | 2 +- homeassistant/components/supla/__init__.py | 4 ++-- homeassistant/components/systemmonitor/sensor.py | 2 +- homeassistant/components/tag/__init__.py | 4 ++-- homeassistant/components/tasmota/device_trigger.py | 2 +- homeassistant/components/wolflink/__init__.py | 2 +- homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/core.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 11f83d8017..b7fd193afa 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -78,7 +78,7 @@ class DiscordNotificationService(BaseNotificationService): ) or discord_bot.get_user(channelid) if channel is None: - _LOGGER.warning("Channel not found for id: %s", channelid) + _LOGGER.warning("Channel not found for ID: %s", channelid) continue # Must create new instances of File for each channel. files = None diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 40313a4155..084811c8fa 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -126,7 +126,7 @@ async def discover_devices(hass, hass_config): if channel_function == SUPLA_FUNCTION_NONE: _LOGGER.debug( - "Ignored function: %s, channel id: %s", + "Ignored function: %s, channel ID: %s", channel_function, channel["id"], ) @@ -136,7 +136,7 @@ async def discover_devices(hass, hass_config): if component_name is None: _LOGGER.warning( - "Unsupported function: %s, channel id: %s", + "Unsupported function: %s, channel ID: %s", channel_function, channel["id"], ) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 1aa6bcdea7..00f193f866 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -268,7 +268,7 @@ class SystemMonitorSensor(Entity): return except psutil.NoSuchProcess as err: _LOGGER.warning( - "Failed to load process with id: %s, old name: %s", + "Failed to load process with ID: %s, old name: %s", err.pid, err.name, ) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 321dce9a29..6c385181aa 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -41,8 +41,8 @@ class TagIDExistsError(HomeAssistantError): """Raised when an item is not found.""" def __init__(self, item_id: str): - """Initialize tag id exists error.""" - super().__init__(f"Tag with id: {item_id} already exists.") + """Initialize tag ID exists error.""" + super().__init__(f"Tag with ID {item_id} already exists.") self.item_id = item_id diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index f06d815e5c..463b1c65a9 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -157,7 +157,7 @@ async def async_setup_trigger(hass, tasmota_trigger, config_entry, discovery_has discovery_id = tasmota_trigger.cfg.trigger_id remove_update_signal = None _LOGGER.debug( - "Discovered trigger with id: %s '%s'", discovery_id, tasmota_trigger.cfg + "Discovered trigger with ID: %s '%s'", discovery_id, tasmota_trigger.cfg ) async def discovery_update(trigger_config): diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 1bfae6cb90..39cd712740 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device_id = entry.data[DEVICE_ID] gateway_id = entry.data[DEVICE_GATEWAY] _LOGGER.debug( - "Setting up wolflink integration for device: %s (id: %s, gateway: %s)", + "Setting up wolflink integration for device: %s (ID: %s, gateway: %s)", device_name, device_id, gateway_id, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 812ac168d4..c57c726972 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -634,7 +634,7 @@ class ZHAGateway: tasks = [] for member in members: _LOGGER.debug( - "Adding member with IEEE: %s and endpoint id: %s to group: %s:0x%04x", + "Adding member with IEEE: %s and endpoint ID: %s to group: %s:0x%04x", member.ieee, member.endpoint_id, name, diff --git a/homeassistant/core.py b/homeassistant/core.py index 01c4047af6..6b657f600d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -863,7 +863,7 @@ class State: if not valid_state(state): raise InvalidStateError( - f"Invalid state encountered for entity id: {entity_id}. " + f"Invalid state encountered for entity ID: {entity_id}. " "State max length is 255 characters." ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ddd1847f6a..7b38c10225 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -464,7 +464,7 @@ class EntityPlatform: # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): entity.add_to_platform_abort() - raise HomeAssistantError(f"Invalid entity id: {entity.entity_id}") + raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}") already_exists = entity.entity_id in self.entities restored = False From 0bc04a650113f96e570b02059afab988fb2fcc84 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 28 Dec 2020 20:05:15 +0100 Subject: [PATCH 239/302] Reset hs color/color temperature when changing the other one (ZHA) (#44566) --- homeassistant/components/zha/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6b3a39d092..4a25fa3c98 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -239,6 +239,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._color_temp = temperature + self._hs_color = None if ( light.ATTR_HS_COLOR in kwargs @@ -254,6 +255,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._hs_color = hs_color + self._color_temp = None if ( effect == light.EFFECT_COLORLOOP From a22d9e54dbbdd8d7fe7e184b678afbadb739d3cd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 28 Dec 2020 22:09:53 +0100 Subject: [PATCH 240/302] Improve TDBU motion blinds control (#44500) * improve TDBU motion blinds control * Simplify service registration --- .../components/motion_blinds/__init__.py | 43 +++++++++- .../components/motion_blinds/const.py | 5 ++ .../components/motion_blinds/cover.py | 79 +++++++++++++++++-- .../components/motion_blinds/services.yaml | 14 ++++ 4 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/motion_blinds/services.yaml diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 95acd6a0c1..2f087cbe52 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -5,28 +5,65 @@ import logging from socket import timeout from motionblinds import MotionMulticast +import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_API_KEY, + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( + ATTR_ABSOLUTE_POSITION, + ATTR_WIDTH, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, MANUFACTURER, MOTION_PLATFORMS, + SERVICE_SET_ABSOLUTE_POSITION, ) from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) +CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) -async def async_setup(hass: core.HomeAssistant, config: dict): +SET_ABSOLUTE_POSITION_SCHEMA = CALL_SCHEMA.extend( + { + vol.Required(ATTR_ABSOLUTE_POSITION): vol.All( + cv.positive_int, vol.Range(max=100) + ), + vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), + } +) + +SERVICE_TO_METHOD = { + SERVICE_SET_ABSOLUTE_POSITION: { + "schema": SET_ABSOLUTE_POSITION_SCHEMA, + } +} + + +def setup(hass: core.HomeAssistant, config: dict): """Set up the Motion Blinds component.""" + + def service_handler(service): + data = service.data.copy() + data["method"] = service.service + dispatcher_send(hass, DOMAIN, data) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.register(DOMAIN, service, service_handler, schema=schema) + return True diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index e5a84041d3..27f2310c7c 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -8,3 +8,8 @@ MOTION_PLATFORMS = ["cover", "sensor"] KEY_GATEWAY = "gateway" KEY_COORDINATOR = "coordinator" KEY_MULTICAST_LISTENER = "multicast_listener" + +ATTR_WIDTH = "width" +ATTR_ABSOLUTE_POSITION = "absolute_position" + +SERVICE_SET_ABSOLUTE_POSITION = "set_absolute_position" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index c1895aa566..06c1f1d273 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -15,10 +15,19 @@ from homeassistant.components.cover import ( DEVICE_CLASS_SHUTTER, CoverEntity, ) +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER +from .const import ( + ATTR_ABSOLUTE_POSITION, + ATTR_WIDTH, + DOMAIN, + KEY_COORDINATOR, + KEY_GATEWAY, + MANUFACTURER, +) _LOGGER = logging.getLogger(__name__) @@ -85,6 +94,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "Bottom", ) ) + entities.append( + MotionTDBUDevice( + coordinator, + blind, + TDBU_DEVICE_MAP[blind.type], + config_entry, + "Combined", + ) + ) else: _LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type) @@ -158,10 +176,28 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self.schedule_update_ha_state(force_refresh=False) async def async_added_to_hass(self): - """Subscribe to multicast pushes.""" + """Subscribe to multicast pushes and register signal handler.""" self._blind.Register_callback(self.unique_id, self._push_callback) + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) + ) await super().async_added_to_hass() + 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 + for key, value in data.items() + if key not in ["entity_id", "method"] + } + getattr(self, data["method"])(**params) + async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" self._blind.Remove_callback(self.unique_id) @@ -180,6 +216,11 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): position = kwargs[ATTR_POSITION] self._blind.Set_position(100 - position) + def set_absolute_position(self, **kwargs): + """Move the cover to a specific absolute position (see TDBU).""" + position = kwargs[ATTR_ABSOLUTE_POSITION] + self._blind.Set_position(100 - position) + def stop_cover(self, **kwargs): """Stop the cover.""" self._blind.Stop() @@ -226,7 +267,7 @@ class MotionTDBUDevice(MotionPositionDevice): self._motor = motor self._motor_key = motor[0] - if self._motor not in ["Bottom", "Top"]: + if self._motor not in ["Bottom", "Top", "Combined"]: _LOGGER.error("Unknown motor '%s'", self._motor) @property @@ -246,10 +287,10 @@ class MotionTDBUDevice(MotionPositionDevice): None is unknown, 0 is open, 100 is closed. """ - if self._blind.position is None: + if self._blind.scaled_position is None: return None - return 100 - self._blind.position[self._motor_key] + return 100 - self._blind.scaled_position[self._motor_key] @property def is_closed(self): @@ -257,8 +298,23 @@ class MotionTDBUDevice(MotionPositionDevice): if self._blind.position is None: return None + if self._motor == "Combined": + return self._blind.width == 100 + return self._blind.position[self._motor_key] == 100 + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attributes = {} + if self._blind.position is not None: + attributes[ATTR_ABSOLUTE_POSITION] = ( + 100 - self._blind.position[self._motor_key] + ) + if self._blind.width is not None: + attributes[ATTR_WIDTH] = self._blind.width + return attributes + def open_cover(self, **kwargs): """Open the cover.""" self._blind.Open(motor=self._motor_key) @@ -268,9 +324,18 @@ class MotionTDBUDevice(MotionPositionDevice): self._blind.Close(motor=self._motor_key) def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" + """Move the cover to a specific scaled position.""" position = kwargs[ATTR_POSITION] - self._blind.Set_position(100 - position, motor=self._motor_key) + self._blind.Set_scaled_position(100 - position, motor=self._motor_key) + + def set_absolute_position(self, **kwargs): + """Move the cover to a specific absolute position.""" + position = kwargs[ATTR_ABSOLUTE_POSITION] + target_width = kwargs.get(ATTR_WIDTH, None) + + self._blind.Set_position( + 100 - position, motor=self._motor_key, width=target_width + ) def stop_cover(self, **kwargs): """Stop the cover.""" diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml new file mode 100644 index 0000000000..f46cc94bd4 --- /dev/null +++ b/homeassistant/components/motion_blinds/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available motion blinds services + +set_absolute_position: + description: "Set the absolute position of the cover." + fields: + entity_id: + description: Name of the motion blind cover entity to control. + example: "cover.TopDownBottomUp-Bottom-0001" + absolute_position: + description: Absolute position to move to. + example: 70 + width: + description: Optionally specify the width that is covered, only for TDBU Combined entities. + example: 30 From ee970230530f3c83d8d7b09196544790e0ad34a9 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Mon, 28 Dec 2020 16:32:04 -0500 Subject: [PATCH 241/302] Add support for Gree device light panels (#42979) --- homeassistant/components/gree/__init__.py | 24 ++- homeassistant/components/gree/bridge.py | 62 +++++++- homeassistant/components/gree/climate.py | 166 +++++++-------------- homeassistant/components/gree/const.py | 1 + homeassistant/components/gree/strings.json | 2 +- homeassistant/components/gree/switch.py | 78 ++++++++++ tests/components/gree/test_climate.py | 156 +------------------ tests/components/gree/test_switch.py | 124 +++++++++++++++ 8 files changed, 342 insertions(+), 271 deletions(-) create mode 100644 homeassistant/components/gree/switch.py create mode 100644 tests/components/gree/test_switch.py diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 96f2401e8d..92b56a4804 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,12 +1,14 @@ """The Gree Climate integration.""" +import asyncio import logging from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .bridge import CannotConnect, DeviceHelper -from .const import DOMAIN +from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper +from .const import COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -40,23 +42,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) devices.append(device) - hass.data[DOMAIN]["devices"] = devices - hass.data[DOMAIN]["pending"] = devices + coordinators = [DeviceDataUpdateCoordinator(hass, d) for d in devices] + await asyncio.gather(*[x.async_refresh() for x in coordinators]) + + hass.data[DOMAIN][COORDINATOR] = coordinators hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) + ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - entry, CLIMATE_DOMAIN + results = asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), + hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), ) + unload_ok = all(await results) if unload_ok: hass.data[DOMAIN].pop("devices", None) - hass.data[DOMAIN].pop("pending", None) + hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None) + hass.data[DOMAIN].pop(SWITCH_DOMAIN, None) return unload_ok diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 44adaf970b..3fbf4a21fb 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -1,11 +1,71 @@ """Helper and wrapper classes for Gree module.""" +from datetime import timedelta +import logging from typing import List from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery -from greeclimate.exceptions import DeviceNotBoundError +from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError from homeassistant import exceptions +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, MAX_ERRORS + +_LOGGER = logging.getLogger(__name__) + + +class DeviceDataUpdateCoordinator(DataUpdateCoordinator): + """Manages polling for state changes from the device.""" + + def __init__(self, hass: HomeAssistant, device: Device): + """Initialize the data update coordinator.""" + DataUpdateCoordinator.__init__( + self, + hass, + _LOGGER, + name=f"{DOMAIN}-{device.device_info.name}", + update_interval=timedelta(seconds=60), + ) + self.device = device + self._error_count = 0 + + async def _async_update_data(self): + """Update the state of the device.""" + try: + await self.device.update_state() + except DeviceTimeoutError as error: + self._error_count += 1 + + # Under normal conditions GREE units timeout every once in a while + if self.last_update_success and self._error_count >= MAX_ERRORS: + _LOGGER.warning( + "Device is unavailable: %s (%s)", + self.name, + self.device.device_info, + ) + raise UpdateFailed(error) from error + else: + if not self.last_update_success and self._error_count: + _LOGGER.warning( + "Device is available: %s (%s)", + self.name, + str(self.device.device_info), + ) + + self._error_count = 0 + + async def push_state_update(self): + """Send state updates to the physical device.""" + try: + return await self.device.push_state_update() + except DeviceTimeoutError: + _LOGGER.warning( + "Timeout send state update to: %s (%s)", + self.name, + self.device.device_info, + ) class DeviceHelper: diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 724903ef36..6a33e3341b 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -1,5 +1,4 @@ """Support for interface with a Gree climate systems.""" -from datetime import timedelta import logging from typing import List @@ -10,7 +9,6 @@ from greeclimate.device import ( TemperatureUnits, VerticalSwing, ) -from greeclimate.exceptions import DeviceTimeoutError from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -45,12 +43,13 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + COORDINATOR, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, - MAX_ERRORS, MAX_TEMP, MIN_TEMP, TARGET_TEMPERATURE_STEP, @@ -58,9 +57,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) -PARALLEL_UPDATES = 0 - HVAC_MODES = { Mode.Auto: HVAC_MODE_AUTO, Mode.Cool: HVAC_MODE_COOL, @@ -101,85 +97,21 @@ SUPPORTED_FEATURES = ( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" async_add_entities( - GreeClimateEntity(device) for device in hass.data[DOMAIN].pop("pending") + [ + GreeClimateEntity(coordinator) + for coordinator in hass.data[DOMAIN][COORDINATOR] + ] ) -class GreeClimateEntity(ClimateEntity): +class GreeClimateEntity(CoordinatorEntity, ClimateEntity): """Representation of a Gree HVAC device.""" - def __init__(self, device): + def __init__(self, coordinator): """Initialize the Gree device.""" - self._device = device - self._name = device.device_info.name - self._mac = device.device_info.mac - self._available = False - self._error_count = 0 - - async def async_update(self): - """Update the state of the device.""" - try: - await self._device.update_state() - - if not self._available and self._error_count: - _LOGGER.warning( - "Device is available: %s (%s)", - self._name, - str(self._device.device_info), - ) - - self._available = True - self._error_count = 0 - except DeviceTimeoutError: - self._error_count += 1 - - # Under normal conditions GREE units timeout every once in a while - if self._available and self._error_count >= MAX_ERRORS: - self._available = False - _LOGGER.warning( - "Device is unavailable: %s (%s)", - self._name, - self._device.device_info, - ) - except Exception: # pylint: disable=broad-except - # Under normal conditions GREE units timeout every once in a while - if self._available: - self._available = False - _LOGGER.exception( - "Unknown exception caught during update by gree device: %s (%s)", - self._name, - self._device.device_info, - ) - - async def _push_state_update(self): - """Send state updates to the physical device.""" - try: - return await self._device.push_state_update() - except DeviceTimeoutError: - self._error_count += 1 - - # Under normal conditions GREE units timeout every once in a while - if self._available and self._error_count >= MAX_ERRORS: - self._available = False - _LOGGER.warning( - "Device timedout while sending state update: %s (%s)", - self._name, - self._device.device_info, - ) - except Exception: # pylint: disable=broad-except - # Under normal conditions GREE units timeout every once in a while - if self._available: - self._available = False - _LOGGER.exception( - "Unknown exception caught while sending state update to: %s (%s)", - self._name, - self._device.device_info, - ) - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._available + super().__init__(coordinator) + self._name = coordinator.device.device_info.name + self._mac = coordinator.device.device_info.mac @property def name(self) -> str: @@ -204,7 +136,7 @@ class GreeClimateEntity(ClimateEntity): @property def temperature_unit(self) -> str: """Return the temperature units for the device.""" - units = self._device.temperature_units + units = self.coordinator.device.temperature_units return TEMP_CELSIUS if units == TemperatureUnits.C else TEMP_FAHRENHEIT @property @@ -220,7 +152,7 @@ class GreeClimateEntity(ClimateEntity): @property def target_temperature(self) -> float: """Return the target temperature for the device.""" - return self._device.target_temperature + return self.coordinator.device.target_temperature async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -234,8 +166,9 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.target_temperature = round(temperature) - await self._push_state_update() + self.coordinator.device.target_temperature = round(temperature) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def min_temp(self) -> float: @@ -255,10 +188,10 @@ class GreeClimateEntity(ClimateEntity): @property def hvac_mode(self) -> str: """Return the current HVAC mode for the device.""" - if not self._device.power: + if not self.coordinator.device.power: return HVAC_MODE_OFF - return HVAC_MODES.get(self._device.mode) + return HVAC_MODES.get(self.coordinator.device.mode) async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" @@ -272,15 +205,17 @@ class GreeClimateEntity(ClimateEntity): ) if hvac_mode == HVAC_MODE_OFF: - self._device.power = False - await self._push_state_update() + self.coordinator.device.power = False + await self.coordinator.push_state_update() + self.async_write_ha_state() return - if not self._device.power: - self._device.power = True + if not self.coordinator.device.power: + self.coordinator.device.power = True - self._device.mode = HVAC_MODES_REVERSE.get(hvac_mode) - await self._push_state_update() + self.coordinator.device.mode = HVAC_MODES_REVERSE.get(hvac_mode) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def hvac_modes(self) -> List[str]: @@ -292,13 +227,13 @@ class GreeClimateEntity(ClimateEntity): @property def preset_mode(self) -> str: """Return the current preset mode for the device.""" - if self._device.steady_heat: + if self.coordinator.device.steady_heat: return PRESET_AWAY - if self._device.power_save: + if self.coordinator.device.power_save: return PRESET_ECO - if self._device.sleep: + if self.coordinator.device.sleep: return PRESET_SLEEP - if self._device.turbo: + if self.coordinator.device.turbo: return PRESET_BOOST return PRESET_NONE @@ -313,21 +248,22 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.steady_heat = False - self._device.power_save = False - self._device.turbo = False - self._device.sleep = False + self.coordinator.device.steady_heat = False + self.coordinator.device.power_save = False + self.coordinator.device.turbo = False + self.coordinator.device.sleep = False if preset_mode == PRESET_AWAY: - self._device.steady_heat = True + self.coordinator.device.steady_heat = True elif preset_mode == PRESET_ECO: - self._device.power_save = True + self.coordinator.device.power_save = True elif preset_mode == PRESET_BOOST: - self._device.turbo = True + self.coordinator.device.turbo = True elif preset_mode == PRESET_SLEEP: - self._device.sleep = True + self.coordinator.device.sleep = True - await self._push_state_update() + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def preset_modes(self) -> List[str]: @@ -337,7 +273,7 @@ class GreeClimateEntity(ClimateEntity): @property def fan_mode(self) -> str: """Return the current fan mode for the device.""" - speed = self._device.fan_speed + speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) async def async_set_fan_mode(self, fan_mode): @@ -345,8 +281,9 @@ class GreeClimateEntity(ClimateEntity): if fan_mode not in FAN_MODES_REVERSE: raise ValueError(f"Invalid fan mode: {fan_mode}") - self._device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) - await self._push_state_update() + self.coordinator.device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def fan_modes(self) -> List[str]: @@ -356,8 +293,8 @@ class GreeClimateEntity(ClimateEntity): @property def swing_mode(self) -> str: """Return the current swing mode for the device.""" - h_swing = self._device.horizontal_swing == HorizontalSwing.FullSwing - v_swing = self._device.vertical_swing == VerticalSwing.FullSwing + h_swing = self.coordinator.device.horizontal_swing == HorizontalSwing.FullSwing + v_swing = self.coordinator.device.vertical_swing == VerticalSwing.FullSwing if h_swing and v_swing: return SWING_BOTH @@ -378,14 +315,15 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.horizontal_swing = HorizontalSwing.Center - self._device.vertical_swing = VerticalSwing.FixedMiddle + self.coordinator.device.horizontal_swing = HorizontalSwing.Center + self.coordinator.device.vertical_swing = VerticalSwing.FixedMiddle if swing_mode in (SWING_BOTH, SWING_HORIZONTAL): - self._device.horizontal_swing = HorizontalSwing.FullSwing + self.coordinator.device.horizontal_swing = HorizontalSwing.FullSwing if swing_mode in (SWING_BOTH, SWING_VERTICAL): - self._device.vertical_swing = VerticalSwing.FullSwing + self.coordinator.device.vertical_swing = VerticalSwing.FullSwing - await self._push_state_update() + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def swing_modes(self) -> List[str]: diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 95435bb3bd..9c64506225 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,6 +1,7 @@ """Constants for the Gree Climate integration.""" DOMAIN = "gree" +COORDINATOR = "coordinator" FAN_MEDIUM_LOW = "medium low" FAN_MEDIUM_HIGH = "medium high" diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json index ad8f0f41ae..9f3518bcf8 100644 --- a/homeassistant/components/gree/strings.json +++ b/homeassistant/components/gree/strings.json @@ -10,4 +10,4 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py new file mode 100644 index 0000000000..f4e9792a58 --- /dev/null +++ b/homeassistant/components/gree/switch.py @@ -0,0 +1,78 @@ +"""Support for interface with a Gree climate systems.""" +import logging +from typing import Optional + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Gree HVAC device from a config entry.""" + async_add_entities( + [ + GreeSwitchEntity(coordinator) + for coordinator in hass.data[DOMAIN][COORDINATOR] + ] + ) + + +class GreeSwitchEntity(CoordinatorEntity, SwitchEntity): + """Representation of a Gree HVAC device.""" + + def __init__(self, coordinator): + """Initialize the Gree device.""" + super().__init__(coordinator) + self._name = coordinator.device.device_info.name + " Panel Light" + self._mac = coordinator.device.device_info.mac + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._name + + @property + def unique_id(self) -> str: + """Return a unique id for the device.""" + return f"{self._mac}-panel-light" + + @property + def icon(self) -> Optional[str]: + """Return the icon for the device.""" + return "mdi:lightbulb" + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self._name, + "identifiers": {(DOMAIN, self._mac)}, + "manufacturer": "Gree", + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + } + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_SWITCH + + @property + def is_on(self) -> bool: + """Return if the light is turned on.""" + return self.coordinator.device.light + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self.coordinator.device.light = True + await self.coordinator.push_state_update() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self.coordinator.device.light = False + await self.coordinator.push_state_update() + self.async_write_ha_state() diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 9f3946e6ad..534168fa78 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -159,10 +159,15 @@ async def test_update_connection_failure(hass, discovery, device, mock_now): async def test_update_connection_failure_recovery(hass, discovery, device, mock_now): """Testing update hvac connection failure recovery.""" - device().update_state.side_effect = [DeviceTimeoutError, DEFAULT_MOCK] + device().update_state.side_effect = [ + DeviceTimeoutError, + DeviceTimeoutError, + DEFAULT_MOCK, + ] await async_setup_gree(hass) + # First update becomes unavailable next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -172,6 +177,7 @@ async def test_update_connection_failure_recovery(hass, discovery, device, mock_ assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE + # Second update restores the connection next_update = mock_now + timedelta(minutes=10) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -188,11 +194,6 @@ async def test_update_unhandled_exception(hass, discovery, device, mock_now): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE @@ -221,21 +222,9 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - device().update_state.side_effect = DeviceTimeoutError device().push_state_update.side_effect = DeviceTimeoutError - # Second update to make an initial error (device is still available) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.name == "fake-device-1" - assert state.state != STATE_UNAVAILABLE - - # Second attempt should make the device unavailable + # Send failure should not raise exceptions or change device state assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -246,47 +235,13 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): state = hass.states.get(ENTITY_ID) assert state is not None - assert state.state == STATE_UNAVAILABLE - - -async def test_send_command_device_unknown_error(hass, discovery, device, mock_now): - """Test for sending power on command to the device with a device timeout.""" - device().update_state.side_effect = [DEFAULT_MOCK, Exception] - device().push_state_update.side_effect = Exception - - await async_setup_gree(hass) - - 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() - - # First update to make the device available - state = hass.states.get(ENTITY_ID) - assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - assert await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state == STATE_UNAVAILABLE - async def test_send_power_on(hass, discovery, device, mock_now): """Test for sending power on command to the device.""" await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -305,11 +260,6 @@ async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -326,11 +276,6 @@ async def test_send_target_temperature(hass, discovery, device, mock_now): """Test for sending target temperature command to the device.""" await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -351,11 +296,6 @@ async def test_send_target_temperature_device_timeout( await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -374,11 +314,6 @@ async def test_update_target_temperature(hass, discovery, device, mock_now): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_TEMPERATURE) == 32 @@ -391,11 +326,6 @@ async def test_send_preset_mode(hass, discovery, device, mock_now, preset): """Test for sending preset mode command to the device.""" await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -412,11 +342,6 @@ async def test_send_invalid_preset_mode(hass, discovery, device, mock_now): """Test for sending preset mode command to the device.""" await async_setup_gree(hass) - 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() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -441,11 +366,6 @@ async def test_send_preset_mode_device_timeout( await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -470,11 +390,6 @@ async def test_update_preset_mode(hass, discovery, device, mock_now, preset): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_PRESET_MODE) == preset @@ -495,11 +410,6 @@ async def test_send_hvac_mode(hass, discovery, device, mock_now, hvac_mode): """Test for sending hvac mode command to the device.""" await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -524,11 +434,6 @@ async def test_send_hvac_mode_device_timeout( await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -559,11 +464,6 @@ async def test_update_hvac_mode(hass, discovery, device, mock_now, hvac_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == hvac_mode @@ -577,11 +477,6 @@ async def test_send_fan_mode(hass, discovery, device, mock_now, fan_mode): """Test for sending fan mode command to the device.""" await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -598,11 +493,6 @@ async def test_send_invalid_fan_mode(hass, discovery, device, mock_now): """Test for sending fan mode command to the device.""" await async_setup_gree(hass) - 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() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -628,11 +518,6 @@ async def test_send_fan_mode_device_timeout( await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -655,11 +540,6 @@ async def test_update_fan_mode(hass, discovery, device, mock_now, fan_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_FAN_MODE) == fan_mode @@ -672,11 +552,6 @@ async def test_send_swing_mode(hass, discovery, device, mock_now, swing_mode): """Test for sending swing mode command to the device.""" await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, @@ -693,11 +568,6 @@ async def test_send_invalid_swing_mode(hass, discovery, device, mock_now): """Test for sending swing mode command to the device.""" await async_setup_gree(hass) - 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() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -722,11 +592,6 @@ async def test_send_swing_mode_device_timeout( await async_setup_gree(hass) - 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 await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, @@ -757,11 +622,6 @@ async def test_update_swing_mode(hass, discovery, device, mock_now, swing_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_SWING_MODE) == swing_mode diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py new file mode 100644 index 0000000000..89a8b224f1 --- /dev/null +++ b/tests/components/gree/test_switch.py @@ -0,0 +1,124 @@ +"""Tests for gree component.""" +from greeclimate.exceptions import DeviceTimeoutError + +from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN +from homeassistant.components.switch import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +ENTITY_ID = f"{DOMAIN}.fake_device_1_panel_light" + + +async def async_setup_gree(hass): + """Set up the gree switch platform.""" + MockConfigEntry(domain=GREE_DOMAIN).add_to_hass(hass) + await async_setup_component(hass, GREE_DOMAIN, {GREE_DOMAIN: {DOMAIN: {}}}) + await hass.async_block_till_done() + + +async def test_send_panel_light_on(hass, discovery, device): + """Test for sending power on command to the device.""" + await async_setup_gree(hass) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_send_panel_light_on_device_timeout(hass, discovery, device): + """Test for sending power on command to the device with a device timeout.""" + device().push_state_update.side_effect = DeviceTimeoutError + + await async_setup_gree(hass) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_send_panel_light_off(hass, discovery, device): + """Test for sending power on command to the device.""" + await async_setup_gree(hass) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + + +async def test_send_panel_light_toggle(hass, discovery, device): + """Test for sending power on command to the device.""" + await async_setup_gree(hass) + + # Turn the service on first + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + # Toggle it off + assert await hass.services.async_call( + DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + + # Toggle is back on + assert await hass.services.async_call( + DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_panel_light_name(hass, discovery, device): + """Test for name property.""" + await async_setup_gree(hass) + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake-device-1 Panel Light" From 0d8ed9061ccc6000995d18d39af2a35bf251fd23 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 28 Dec 2020 23:04:17 +0100 Subject: [PATCH 242/302] Update pyotgw to 1.0b1 (#43352) Co-authored-by: Martin Hjelmare --- .../components/opentherm_gw/binary_sensor.py | 93 +++- .../components/opentherm_gw/climate.py | 22 +- .../components/opentherm_gw/config_flow.py | 2 +- .../components/opentherm_gw/const.py | 491 +++++++++++++++--- .../components/opentherm_gw/manifest.json | 2 +- .../components/opentherm_gw/sensor.py | 97 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../opentherm_gw/test_config_flow.py | 13 +- 9 files changed, 612 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 9e3c4d4122..a896b37a26 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -1,14 +1,22 @@ """Support for OpenTherm Gateway binary sensors.""" import logging +from pprint import pformat from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN -from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW +from .const import ( + BINARY_SENSOR_INFO, + DATA_GATEWAYS, + DATA_OPENTHERM_GW, + DEPRECATED_BINARY_SENSOR_SOURCE_LOOKUP, + TRANSLATE_SOURCE, +) _LOGGER = logging.getLogger(__name__) @@ -16,16 +24,51 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" sensors = [] + deprecated_sensors = [] + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ent_reg = await async_get_registry(hass) for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] friendly_name_format = info[1] - sensors.append( - OpenThermBinarySensor( - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], - var, - device_class, - friendly_name_format, + status_sources = info[2] + + for source in status_sources: + sensors.append( + OpenThermBinarySensor( + gw_dev, + var, + source, + device_class, + friendly_name_format, + ) ) + + old_style_entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + old_ent = ent_reg.async_get(old_style_entity_id) + if old_ent and old_ent.config_entry_id == config_entry.entry_id: + if old_ent.disabled: + ent_reg.async_remove(old_style_entity_id) + else: + deprecated_sensors.append( + DeprecatedOpenThermBinarySensor( + gw_dev, + var, + device_class, + friendly_name_format, + ) + ) + + sensors.extend(deprecated_sensors) + + if deprecated_sensors: + _LOGGER.warning( + "The following binary_sensor entities are deprecated and may " + "no longer behave as expected. They will be removed in a " + "future version. You can force removal of these entities by " + "disabling them and restarting Home Assistant.\n%s", + pformat([s.entity_id for s in deprecated_sensors]), ) async_add_entities(sensors) @@ -34,15 +77,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class OpenThermBinarySensor(BinarySensorEntity): """Represent an OpenTherm Gateway binary sensor.""" - def __init__(self, gw_dev, var, device_class, friendly_name_format): + def __init__(self, gw_dev, var, source, device_class, friendly_name_format): """Initialize the binary sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{source}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var + self._source = source self._state = None self._device_class = device_class + if TRANSLATE_SOURCE[source] is not None: + friendly_name_format = ( + f"{friendly_name_format} ({TRANSLATE_SOURCE[source]})" + ) self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None @@ -73,7 +121,7 @@ class OpenThermBinarySensor(BinarySensorEntity): @callback def receive_report(self, status): """Handle status updates from the component.""" - state = status.get(self._var) + state = status[self._source].get(self._var) self._state = None if state is None else bool(state) self.async_write_ha_state() @@ -96,7 +144,7 @@ class OpenThermBinarySensor(BinarySensorEntity): @property def unique_id(self): """Return a unique ID.""" - return f"{self._gateway.gw_id}-{self._var}" + return f"{self._gateway.gw_id}-{self._source}-{self._var}" @property def is_on(self): @@ -112,3 +160,26 @@ class OpenThermBinarySensor(BinarySensorEntity): def should_poll(self): """Return False because entity pushes its state.""" return False + + +class DeprecatedOpenThermBinarySensor(OpenThermBinarySensor): + """Represent a deprecated OpenTherm Gateway Binary Sensor.""" + + # pylint: disable=super-init-not-called + def __init__(self, gw_dev, var, device_class, friendly_name_format): + """Initialize the binary sensor.""" + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + self._gateway = gw_dev + self._var = var + self._source = DEPRECATED_BINARY_SENSOR_SOURCE_LOOKUP[var] + self._state = None + self._device_class = device_class + self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 237733e687..8ec536e733 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -101,10 +101,10 @@ class OpenThermClimate(ClimateEntity): @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" - self._available = bool(status) - ch_active = status.get(gw_vars.DATA_SLAVE_CH_ACTIVE) - flame_on = status.get(gw_vars.DATA_SLAVE_FLAME_ON) - cooling_active = status.get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) + self._available = status != gw_vars.DEFAULT_STATUS + ch_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE) + flame_on = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON) + cooling_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) if ch_active and flame_on: self._current_operation = CURRENT_HVAC_HEAT self._hvac_mode = HVAC_MODE_HEAT @@ -114,8 +114,10 @@ class OpenThermClimate(ClimateEntity): else: self._current_operation = CURRENT_HVAC_IDLE - self._current_temperature = status.get(gw_vars.DATA_ROOM_TEMP) - temp_upd = status.get(gw_vars.DATA_ROOM_SETPOINT) + self._current_temperature = status[gw_vars.THERMOSTAT].get( + gw_vars.DATA_ROOM_TEMP + ) + temp_upd = status[gw_vars.THERMOSTAT].get(gw_vars.DATA_ROOM_SETPOINT) if self._target_temperature != temp_upd: self._new_target_temperature = None @@ -123,14 +125,14 @@ class OpenThermClimate(ClimateEntity): # GPIO mode 5: 0 == Away # GPIO mode 6: 1 == Away - gpio_a_state = status.get(gw_vars.OTGW_GPIO_A) + gpio_a_state = status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_A) if gpio_a_state == 5: self._away_mode_a = 0 elif gpio_a_state == 6: self._away_mode_a = 1 else: self._away_mode_a = None - gpio_b_state = status.get(gw_vars.OTGW_GPIO_B) + gpio_b_state = status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_B) if gpio_b_state == 5: self._away_mode_b = 0 elif gpio_b_state == 6: @@ -139,11 +141,11 @@ class OpenThermClimate(ClimateEntity): self._away_mode_b = None if self._away_mode_a is not None: self._away_state_a = ( - status.get(gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a + status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a ) if self._away_mode_b is not None: self._away_state_b = ( - status.get(gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b + status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b ) self.async_write_ha_state() diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 59f14ab2ee..8da530bebd 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -54,7 +54,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.pyotgw() status = await otgw.connect(self.hass.loop, device) await otgw.disconnect() - return status.get(gw_vars.OTGW_ABOUT) + return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: res = await asyncio.wait_for(test_connection(), timeout=10) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 3ff1577c43..2c3e2f7071 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -40,244 +40,599 @@ SERVICE_SET_MAX_MOD = "set_max_modulation" SERVICE_SET_OAT = "set_outside_temperature" SERVICE_SET_SB_TEMP = "set_setback_temperature" +TRANSLATE_SOURCE = { + gw_vars.BOILER: "Boiler", + gw_vars.OTGW: None, + gw_vars.THERMOSTAT: "Thermostat", +} + UNIT_KW = "kW" UNIT_L_MIN = f"L/{TIME_MINUTES}" BINARY_SENSOR_INFO = { - # [device_class, friendly_name format] - gw_vars.DATA_MASTER_CH_ENABLED: [None, "Thermostat Central Heating Enabled {}"], - gw_vars.DATA_MASTER_DHW_ENABLED: [None, "Thermostat Hot Water Enabled {}"], - gw_vars.DATA_MASTER_COOLING_ENABLED: [None, "Thermostat Cooling Enabled {}"], + # [device_class, friendly_name format, [status source, ...]] + gw_vars.DATA_MASTER_CH_ENABLED: [ + None, + "Thermostat Central Heating {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_DHW_ENABLED: [ + None, + "Thermostat Hot Water {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_COOLING_ENABLED: [ + None, + "Thermostat Cooling {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], gw_vars.DATA_MASTER_OTC_ENABLED: [ None, - "Thermostat Outside Temperature Correction Enabled {}", + "Thermostat Outside Temperature Correction {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_CH2_ENABLED: [ + None, + "Thermostat Central Heating 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_FAULT_IND: [ + DEVICE_CLASS_PROBLEM, + "Boiler Fault {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_MASTER_CH2_ENABLED: [None, "Thermostat Central Heating 2 Enabled {}"], - gw_vars.DATA_SLAVE_FAULT_IND: [DEVICE_CLASS_PROBLEM, "Boiler Fault Indication {}"], gw_vars.DATA_SLAVE_CH_ACTIVE: [ DEVICE_CLASS_HEAT, - "Boiler Central Heating Status {}", + "Boiler Central Heating {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_DHW_ACTIVE: [ + DEVICE_CLASS_HEAT, + "Boiler Hot Water {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_FLAME_ON: [ + DEVICE_CLASS_HEAT, + "Boiler Flame {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ + DEVICE_CLASS_COLD, + "Boiler Cooling {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_DHW_ACTIVE: [DEVICE_CLASS_HEAT, "Boiler Hot Water Status {}"], - gw_vars.DATA_SLAVE_FLAME_ON: [DEVICE_CLASS_HEAT, "Boiler Flame Status {}"], - gw_vars.DATA_SLAVE_COOLING_ACTIVE: [DEVICE_CLASS_COLD, "Boiler Cooling Status {}"], gw_vars.DATA_SLAVE_CH2_ACTIVE: [ DEVICE_CLASS_HEAT, - "Boiler Central Heating 2 Status {}", + "Boiler Central Heating 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DIAG_IND: [ DEVICE_CLASS_PROBLEM, - "Boiler Diagnostics Indication {}", + "Boiler Diagnostics {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_DHW_PRESENT: [ + None, + "Boiler Hot Water Present {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_CONTROL_TYPE: [ + None, + "Boiler Control Type {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [ + None, + "Boiler Cooling Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_DHW_CONFIG: [ + None, + "Boiler Hot Water Configuration {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [ + None, + "Boiler Pump Commands Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_CH2_PRESENT: [ + None, + "Boiler Central Heating 2 Present {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present {}"], - gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type {}"], - gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support {}"], - gw_vars.DATA_SLAVE_DHW_CONFIG: [None, "Boiler Hot Water Configuration {}"], - gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [None, "Boiler Pump Commands Support {}"], - gw_vars.DATA_SLAVE_CH2_PRESENT: [None, "Boiler Central Heating 2 Present {}"], gw_vars.DATA_SLAVE_SERVICE_REQ: [ DEVICE_CLASS_PROBLEM, "Boiler Service Required {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_REMOTE_RESET: [ + None, + "Boiler Remote Reset Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support {}"], gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_GAS_FAULT: [ + DEVICE_CLASS_PROBLEM, + "Boiler Gas Fault {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_GAS_FAULT: [DEVICE_CLASS_PROBLEM, "Boiler Gas Fault {}"], gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_REMOTE_TRANSFER_DHW: [ None, "Remote Hot Water Setpoint Transfer Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [ None, "Remote Maximum Central Heating Setpoint Write Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_REMOTE_RW_DHW: [ + None, + "Remote Hot Water Setpoint Write Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_REMOTE_RW_DHW: [None, "Remote Hot Water Setpoint Write Support {}"], gw_vars.DATA_REMOTE_RW_MAX_CH: [ None, "Remote Central Heating Setpoint Write Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_ROVRD_MAN_PRIO: [None, "Remote Override Manual Change Priority {}"], - gw_vars.DATA_ROVRD_AUTO_PRIO: [None, "Remote Override Program Change Priority {}"], - gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State {}"], - gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State {}"], - gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions {}"], - gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte {}"], + gw_vars.DATA_ROVRD_MAN_PRIO: [ + None, + "Remote Override Manual Change Priority {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_ROVRD_AUTO_PRIO: [ + None, + "Remote Override Program Change Priority {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A {}", [gw_vars.OTGW]], + gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B {}", [gw_vars.OTGW]], + gw_vars.OTGW_IGNORE_TRANSITIONS: [ + None, + "Gateway Ignore Transitions {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte {}", [gw_vars.OTGW]], } SENSOR_INFO = { - # [device_class, unit, friendly_name] + # [device_class, unit, friendly_name, [status source, ...]] gw_vars.DATA_CONTROL_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_MEMBERID: [ + None, + None, + "Thermostat Member ID {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_MEMBERID: [ + None, + None, + "Boiler Member ID {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_OEM_FAULT: [ + None, + None, + "Boiler OEM Fault Code {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_COOLING_CONTROL: [ + None, + PERCENTAGE, + "Cooling Control Signal {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_MASTER_MEMBERID: [None, None, "Thermostat Member ID {}"], - gw_vars.DATA_SLAVE_MEMBERID: [None, None, "Boiler Member ID {}"], - gw_vars.DATA_SLAVE_OEM_FAULT: [None, None, "Boiler OEM Fault Code {}"], - gw_vars.DATA_COOLING_CONTROL: [None, PERCENTAGE, "Cooling Control Signal {}"], gw_vars.DATA_CONTROL_SETPOINT_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT_OVRD: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint Override {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: [ None, PERCENTAGE, "Boiler Maximum Relative Modulation {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_MAX_CAPACITY: [ + None, + UNIT_KW, + "Boiler Maximum Capacity {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_MAX_CAPACITY: [None, UNIT_KW, "Boiler Maximum Capacity {}"], gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: [ None, PERCENTAGE, "Boiler Minimum Modulation Level {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_REL_MOD_LEVEL: [ + None, + PERCENTAGE, + "Relative Modulation Level {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_REL_MOD_LEVEL: [None, PERCENTAGE, "Relative Modulation Level {}"], gw_vars.DATA_CH_WATER_PRESS: [ None, PRESSURE_BAR, "Central Heating Water Pressure {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_FLOW_RATE: [ + None, + UNIT_L_MIN, + "Hot Water Flow Rate {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate {}"], gw_vars.DATA_ROOM_SETPOINT_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CH_WATER_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Central Heating Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_OUTSIDE_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Outside Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_RETURN_WATER_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Return Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SOLAR_STORAGE_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Solar Storage Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SOLAR_COLL_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Solar Collector Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CH_WATER_TEMP_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Central Heating 2 Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_TEMP_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water 2 Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_EXHAUST_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Exhaust Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_MAX_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Maximum Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_MIN_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Minimum Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_MAX_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Boiler Maximum Central Heating Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_MIN_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Boiler Minimum Central Heating Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_MAX_CH_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Maximum Central Heating Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_OEM_DIAG: [None, None, "OEM Diagnostic Code {}"], - gw_vars.DATA_TOTAL_BURNER_STARTS: [None, None, "Total Burner Starts {}"], - gw_vars.DATA_CH_PUMP_STARTS: [None, None, "Central Heating Pump Starts {}"], - gw_vars.DATA_DHW_PUMP_STARTS: [None, None, "Hot Water Pump Starts {}"], - gw_vars.DATA_DHW_BURNER_STARTS: [None, None, "Hot Water Burner Starts {}"], - gw_vars.DATA_TOTAL_BURNER_HOURS: [None, TIME_HOURS, "Total Burner Hours {}"], - gw_vars.DATA_CH_PUMP_HOURS: [None, TIME_HOURS, "Central Heating Pump Hours {}"], - gw_vars.DATA_DHW_PUMP_HOURS: [None, TIME_HOURS, "Hot Water Pump Hours {}"], - gw_vars.DATA_DHW_BURNER_HOURS: [None, TIME_HOURS, "Hot Water Burner Hours {}"], - gw_vars.DATA_MASTER_OT_VERSION: [None, None, "Thermostat OpenTherm Version {}"], - gw_vars.DATA_SLAVE_OT_VERSION: [None, None, "Boiler OpenTherm Version {}"], - gw_vars.DATA_MASTER_PRODUCT_TYPE: [None, None, "Thermostat Product Type {}"], - gw_vars.DATA_MASTER_PRODUCT_VERSION: [None, None, "Thermostat Product Version {}"], - gw_vars.DATA_SLAVE_PRODUCT_TYPE: [None, None, "Boiler Product Type {}"], - gw_vars.DATA_SLAVE_PRODUCT_VERSION: [None, None, "Boiler Product Version {}"], - gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode {}"], - gw_vars.OTGW_DHW_OVRD: [None, None, "Gateway Hot Water Override Mode {}"], - gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version {}"], - gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build {}"], - gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed {}"], - gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode {}"], - gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode {}"], - gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode {}"], - gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode {}"], - gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode {}"], - gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode {}"], - gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode {}"], - gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode {}"], + gw_vars.DATA_OEM_DIAG: [ + None, + None, + "OEM Diagnostic Code {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_TOTAL_BURNER_STARTS: [ + None, + None, + "Total Burner Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_CH_PUMP_STARTS: [ + None, + None, + "Central Heating Pump Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_PUMP_STARTS: [ + None, + None, + "Hot Water Pump Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_BURNER_STARTS: [ + None, + None, + "Hot Water Burner Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_TOTAL_BURNER_HOURS: [ + None, + TIME_HOURS, + "Total Burner Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_CH_PUMP_HOURS: [ + None, + TIME_HOURS, + "Central Heating Pump Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_PUMP_HOURS: [ + None, + TIME_HOURS, + "Hot Water Pump Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_BURNER_HOURS: [ + None, + TIME_HOURS, + "Hot Water Burner Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_OT_VERSION: [ + None, + None, + "Thermostat OpenTherm Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_OT_VERSION: [ + None, + None, + "Boiler OpenTherm Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_PRODUCT_TYPE: [ + None, + None, + "Thermostat Product Type {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_PRODUCT_VERSION: [ + None, + None, + "Thermostat Product Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_PRODUCT_TYPE: [ + None, + None, + "Boiler Product Type {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_PRODUCT_VERSION: [ + None, + None, + "Boiler Product Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_DHW_OVRD: [ + None, + None, + "Gateway Hot Water Override Mode {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version {}", [gw_vars.OTGW]], + gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build {}", [gw_vars.OTGW]], + gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode {}", [gw_vars.OTGW]], gw_vars.OTGW_SB_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Gateway Setback Temperature {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_SETP_OVRD_MODE: [ + None, + None, + "Gateway Room Setpoint Override Mode {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_THRM_DETECT: [ + None, + None, + "Gateway Thermostat Detection {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_VREF: [ + None, + None, + "Gateway Reference Voltage Setting {}", + [gw_vars.OTGW], ], - gw_vars.OTGW_SETP_OVRD_MODE: [None, None, "Gateway Room Setpoint Override Mode {}"], - gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode {}"], - gw_vars.OTGW_THRM_DETECT: [None, None, "Gateway Thermostat Detection {}"], - gw_vars.OTGW_VREF: [None, None, "Gateway Reference Voltage Setting {}"], +} + +DEPRECATED_BINARY_SENSOR_SOURCE_LOOKUP = { + gw_vars.DATA_MASTER_CH_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_DHW_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_OTC_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_CH2_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_FAULT_IND: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_FLAME_ON: gw_vars.BOILER, + gw_vars.DATA_SLAVE_COOLING_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH2_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DIAG_IND: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_PRESENT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CONTROL_TYPE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_COOLING_SUPPORTED: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_CONFIG: gw_vars.BOILER, + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH2_PRESENT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_SERVICE_REQ: gw_vars.BOILER, + gw_vars.DATA_SLAVE_REMOTE_RESET: gw_vars.BOILER, + gw_vars.DATA_SLAVE_LOW_WATER_PRESS: gw_vars.BOILER, + gw_vars.DATA_SLAVE_GAS_FAULT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_WATER_OVERTEMP: gw_vars.BOILER, + gw_vars.DATA_REMOTE_TRANSFER_DHW: gw_vars.BOILER, + gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: gw_vars.BOILER, + gw_vars.DATA_REMOTE_RW_DHW: gw_vars.BOILER, + gw_vars.DATA_REMOTE_RW_MAX_CH: gw_vars.BOILER, + gw_vars.DATA_ROVRD_MAN_PRIO: gw_vars.THERMOSTAT, + gw_vars.DATA_ROVRD_AUTO_PRIO: gw_vars.THERMOSTAT, + gw_vars.OTGW_GPIO_A_STATE: gw_vars.OTGW, + gw_vars.OTGW_GPIO_B_STATE: gw_vars.OTGW, + gw_vars.OTGW_IGNORE_TRANSITIONS: gw_vars.OTGW, + gw_vars.OTGW_OVRD_HB: gw_vars.OTGW, +} + +DEPRECATED_SENSOR_SOURCE_LOOKUP = { + gw_vars.DATA_CONTROL_SETPOINT: gw_vars.BOILER, + gw_vars.DATA_MASTER_MEMBERID: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_MEMBERID: gw_vars.BOILER, + gw_vars.DATA_SLAVE_OEM_FAULT: gw_vars.BOILER, + gw_vars.DATA_COOLING_CONTROL: gw_vars.BOILER, + gw_vars.DATA_CONTROL_SETPOINT_2: gw_vars.BOILER, + gw_vars.DATA_ROOM_SETPOINT_OVRD: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: gw_vars.BOILER, + gw_vars.DATA_SLAVE_MAX_CAPACITY: gw_vars.BOILER, + gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: gw_vars.BOILER, + gw_vars.DATA_ROOM_SETPOINT: gw_vars.THERMOSTAT, + gw_vars.DATA_REL_MOD_LEVEL: gw_vars.BOILER, + gw_vars.DATA_CH_WATER_PRESS: gw_vars.BOILER, + gw_vars.DATA_DHW_FLOW_RATE: gw_vars.BOILER, + gw_vars.DATA_ROOM_SETPOINT_2: gw_vars.THERMOSTAT, + gw_vars.DATA_ROOM_TEMP: gw_vars.THERMOSTAT, + gw_vars.DATA_CH_WATER_TEMP: gw_vars.BOILER, + gw_vars.DATA_DHW_TEMP: gw_vars.BOILER, + gw_vars.DATA_OUTSIDE_TEMP: gw_vars.THERMOSTAT, + gw_vars.DATA_RETURN_WATER_TEMP: gw_vars.BOILER, + gw_vars.DATA_SOLAR_STORAGE_TEMP: gw_vars.BOILER, + gw_vars.DATA_SOLAR_COLL_TEMP: gw_vars.BOILER, + gw_vars.DATA_CH_WATER_TEMP_2: gw_vars.BOILER, + gw_vars.DATA_DHW_TEMP_2: gw_vars.BOILER, + gw_vars.DATA_EXHAUST_TEMP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_MAX_SETP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_MIN_SETP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH_MAX_SETP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH_MIN_SETP: gw_vars.BOILER, + gw_vars.DATA_DHW_SETPOINT: gw_vars.BOILER, + gw_vars.DATA_MAX_CH_SETPOINT: gw_vars.BOILER, + gw_vars.DATA_OEM_DIAG: gw_vars.BOILER, + gw_vars.DATA_TOTAL_BURNER_STARTS: gw_vars.BOILER, + gw_vars.DATA_CH_PUMP_STARTS: gw_vars.BOILER, + gw_vars.DATA_DHW_PUMP_STARTS: gw_vars.BOILER, + gw_vars.DATA_DHW_BURNER_STARTS: gw_vars.BOILER, + gw_vars.DATA_TOTAL_BURNER_HOURS: gw_vars.BOILER, + gw_vars.DATA_CH_PUMP_HOURS: gw_vars.BOILER, + gw_vars.DATA_DHW_PUMP_HOURS: gw_vars.BOILER, + gw_vars.DATA_DHW_BURNER_HOURS: gw_vars.BOILER, + gw_vars.DATA_MASTER_OT_VERSION: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_OT_VERSION: gw_vars.BOILER, + gw_vars.DATA_MASTER_PRODUCT_TYPE: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_PRODUCT_VERSION: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_PRODUCT_TYPE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_PRODUCT_VERSION: gw_vars.BOILER, + gw_vars.OTGW_MODE: gw_vars.OTGW, + gw_vars.OTGW_DHW_OVRD: gw_vars.OTGW, + gw_vars.OTGW_ABOUT: gw_vars.OTGW, + gw_vars.OTGW_BUILD: gw_vars.OTGW, + gw_vars.OTGW_CLOCKMHZ: gw_vars.OTGW, + gw_vars.OTGW_LED_A: gw_vars.OTGW, + gw_vars.OTGW_LED_B: gw_vars.OTGW, + gw_vars.OTGW_LED_C: gw_vars.OTGW, + gw_vars.OTGW_LED_D: gw_vars.OTGW, + gw_vars.OTGW_LED_E: gw_vars.OTGW, + gw_vars.OTGW_LED_F: gw_vars.OTGW, + gw_vars.OTGW_GPIO_A: gw_vars.OTGW, + gw_vars.OTGW_GPIO_B: gw_vars.OTGW, + gw_vars.OTGW_SB_TEMP: gw_vars.OTGW, + gw_vars.OTGW_SETP_OVRD_MODE: gw_vars.OTGW, + gw_vars.OTGW_SMART_PWR: gw_vars.OTGW, + gw_vars.OTGW_THRM_DETECT: gw_vars.OTGW, + gw_vars.OTGW_VREF: gw_vars.OTGW, } diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 558f4adced..066cee61c0 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==0.6b1"], + "requirements": ["pyotgw==1.0b1"], "codeowners": ["@mvn23"], "config_flow": true } diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index b2f8e27298..4a20aa651c 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -1,14 +1,22 @@ """Support for OpenTherm Gateway sensors.""" import logging +from pprint import pformat from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN -from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO +from .const import ( + DATA_GATEWAYS, + DATA_OPENTHERM_GW, + DEPRECATED_SENSOR_SOURCE_LOOKUP, + SENSOR_INFO, + TRANSLATE_SOURCE, +) _LOGGER = logging.getLogger(__name__) @@ -16,18 +24,54 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" sensors = [] + deprecated_sensors = [] + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ent_reg = await async_get_registry(hass) for var, info in SENSOR_INFO.items(): device_class = info[0] unit = info[1] friendly_name_format = info[2] - sensors.append( - OpenThermSensor( - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], - var, - device_class, - unit, - friendly_name_format, + status_sources = info[3] + + for source in status_sources: + sensors.append( + OpenThermSensor( + gw_dev, + var, + source, + device_class, + unit, + friendly_name_format, + ) ) + + old_style_entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + old_ent = ent_reg.async_get(old_style_entity_id) + if old_ent and old_ent.config_entry_id == config_entry.entry_id: + if old_ent.disabled: + ent_reg.async_remove(old_style_entity_id) + else: + deprecated_sensors.append( + DeprecatedOpenThermSensor( + gw_dev, + var, + device_class, + unit, + friendly_name_format, + ) + ) + + sensors.extend(deprecated_sensors) + + if deprecated_sensors: + _LOGGER.warning( + "The following sensor entities are deprecated and may no " + "longer behave as expected. They will be removed in a future " + "version. You can force removal of these entities by disabling " + "them and restarting Home Assistant.\n%s", + pformat([s.entity_id for s in deprecated_sensors]), ) async_add_entities(sensors) @@ -36,16 +80,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class OpenThermSensor(Entity): """Representation of an OpenTherm Gateway sensor.""" - def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): + def __init__(self, gw_dev, var, source, device_class, unit, friendly_name_format): """Initialize the OpenTherm Gateway sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{source}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var + self._source = source self._value = None self._device_class = device_class self._unit = unit + if TRANSLATE_SOURCE[source] is not None: + friendly_name_format = ( + f"{friendly_name_format} ({TRANSLATE_SOURCE[source]})" + ) self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None @@ -74,7 +123,7 @@ class OpenThermSensor(Entity): @callback def receive_report(self, status): """Handle status updates from the component.""" - value = status.get(self._var) + value = status[self._source].get(self._var) if isinstance(value, float): value = f"{value:2.1f}" self._value = value @@ -99,7 +148,7 @@ class OpenThermSensor(Entity): @property def unique_id(self): """Return a unique ID.""" - return f"{self._gateway.gw_id}-{self._var}" + return f"{self._gateway.gw_id}-{self._source}-{self._var}" @property def device_class(self): @@ -120,3 +169,27 @@ class OpenThermSensor(Entity): def should_poll(self): """Return False because entity pushes its state.""" return False + + +class DeprecatedOpenThermSensor(OpenThermSensor): + """Represent a deprecated OpenTherm Gateway Sensor.""" + + # pylint: disable=super-init-not-called + def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): + """Initialize the OpenTherm Gateway sensor.""" + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + self._gateway = gw_dev + self._var = var + self._source = DEPRECATED_SENSOR_SOURCE_LOOKUP[var] + self._value = None + self._device_class = device_class + self._unit = unit + self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" diff --git a/requirements_all.txt b/requirements_all.txt index 917006cea9..5bb2f37d9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1587,7 +1587,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.6b1 +pyotgw==1.0b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 485caf8bfc..7ff9bd8c97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -797,7 +797,7 @@ pyopenuv==1.0.9 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==0.6b1 +pyotgw==1.0b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index a7be6ddcf6..e0f4c6eda9 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Opentherm Gateway config flow.""" import asyncio -from pyotgw.vars import OTGW_ABOUT +from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup @@ -15,6 +15,8 @@ from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVE from tests.async_mock import patch from tests.common import MockConfigEntry +MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}} + async def test_form_user(hass): """Test we get the form.""" @@ -32,8 +34,7 @@ async def test_form_user(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: @@ -65,8 +66,7 @@ async def test_form_import(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: @@ -108,8 +108,7 @@ async def test_form_duplicate_entries(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: From a5cd4efd83a524d72b250a7324cddbf66d5f58db Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 14:58:09 -0800 Subject: [PATCH 243/302] Optimize api calls between envoy_reader and Home Assistant (#42857) * Updating sensor to use single API call * Updated comment Updated comment to reflect that polling is needed. * Reduced calls to single API call * Added except handling and increased async timeout * Cleaned up some comments * Added error handling * Added last_reported date for inverters * Added message during failed update * Added retries to update function * Updated update function * Reformatted sensor.py with black * Increased default scan period * fixed timedelta typo * importing CoordinatorEntity * Check during setup else raise PlatformNotReady * Removed async_update and override state * using SCAN_INTERVAL constant * fixed typo * removed unused constant * Removed retry logic * Changed to catching exceptions rather than strings * shortened string split line * Replace requests_async with httpx * Bump envoy_reader version to 0.17.2 * Resolving comments from PR requested changes * Fixed typo in scan_interval * Removed period from logging messages * Bumping envoy_reader to 0.18.0 * Incorporating suggested changes * Removing no longer used try/except * Fail setup if authentication fails * Bump envoy_reader to 0.18.2 --- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 178 +++++++++++------- requirements_all.txt | 2 +- 3 files changed, 115 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index b339013a69..87491a7254 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,7 +2,7 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.17.3"], + "requirements": ["envoy_reader==0.18.2"], "codeowners": [ "@gtdiehl" ] diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index a2b50f20eb..64b4fdf66a 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,8 +1,11 @@ """Support for Enphase Envoy solar energy monitor.""" + +from datetime import timedelta import logging +import async_timeout from envoy_reader.envoy_reader import EnvoyReader -import requests +import httpx import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -15,8 +18,13 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, POWER_WATT, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) _LOGGER = logging.getLogger(__name__) @@ -38,10 +46,11 @@ SENSORS = { "inverters": ("Envoy Inverter", POWER_WATT), } - ICON = "mdi:flash" CONST_DEFAULT_HOST = "envoy" +SCAN_INTERVAL = timedelta(seconds=60) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, @@ -55,7 +64,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + homeassistant, config, async_add_entities, discovery_info=None +): """Set up the Enphase Envoy sensor.""" ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] @@ -63,55 +74,99 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - envoy_reader = EnvoyReader(ip_address, username, password) + if "inverters" in monitored_conditions: + envoy_reader = EnvoyReader(ip_address, username, password, inverters=True) + else: + envoy_reader = EnvoyReader(ip_address, username, password) + + try: + await envoy_reader.getData() + except httpx.HTTPStatusError as err: + _LOGGER.error("Authentication failure during setup: %s", err) + return + except httpx.HTTPError as err: + raise PlatformNotReady from err + + async def async_update_data(): + """Fetch data from API endpoint.""" + data = {} + async with async_timeout.timeout(30): + try: + await envoy_reader.getData() + except httpx.HTTPError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + for condition in monitored_conditions: + if condition != "inverters": + data[condition] = await getattr(envoy_reader, condition)() + else: + data["inverters_production"] = await getattr( + envoy_reader, "inverters_production" + )() + + _LOGGER.debug("Retrieved data from API: %s", data) + + return data + + coordinator = DataUpdateCoordinator( + homeassistant, + _LOGGER, + name="sensor", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_refresh() + + if coordinator.data is None: + raise PlatformNotReady entities = [] - # Iterate through the list of sensors for condition in monitored_conditions: - if condition == "inverters": - try: - inverters = await envoy_reader.inverters_production() - except requests.exceptions.HTTPError: - _LOGGER.warning( - "Authentication for Inverter data failed during setup: %s", - ip_address, - ) - continue - - if isinstance(inverters, dict): - for inverter in inverters: - entities.append( - Envoy( - envoy_reader, - condition, - f"{name}{SENSORS[condition][0]} {inverter}", - SENSORS[condition][1], - ) + entity_name = "" + if ( + condition == "inverters" + and coordinator.data.get("inverters_production") is not None + ): + for inverter in coordinator.data["inverters_production"]: + entity_name = f"{name}{SENSORS[condition][0]} {inverter}" + split_name = entity_name.split(" ") + serial_number = split_name[-1] + entities.append( + Envoy( + condition, + entity_name, + serial_number, + SENSORS[condition][1], + coordinator, ) - - else: + ) + elif condition != "inverters": + entity_name = f"{name}{SENSORS[condition][0]}" entities.append( Envoy( - envoy_reader, condition, - f"{name}{SENSORS[condition][0]}", + entity_name, + None, SENSORS[condition][1], + coordinator, ) ) + async_add_entities(entities) -class Envoy(Entity): - """Implementation of the Enphase Envoy sensors.""" +class Envoy(CoordinatorEntity): + """Envoy entity.""" - def __init__(self, envoy_reader, sensor_type, name, unit): - """Initialize the sensor.""" - self._envoy_reader = envoy_reader + def __init__(self, sensor_type, name, serial_number, unit, coordinator): + """Initialize Envoy entity.""" self._type = sensor_type self._name = name + self._serial_number = serial_number self._unit_of_measurement = unit - self._state = None - self._last_reported = None + + super().__init__(coordinator) @property def name(self): @@ -121,7 +176,20 @@ class Envoy(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + if self._type != "inverters": + value = self.coordinator.data.get(self._type) + + elif ( + self._type == "inverters" + and self.coordinator.data.get("inverters_production") is not None + ): + value = self.coordinator.data.get("inverters_production").get( + self._serial_number + )[0] + else: + return None + + return value @property def unit_of_measurement(self): @@ -136,33 +204,13 @@ class Envoy(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if self._type == "inverters": - return {"last_reported": self._last_reported} + if ( + self._type == "inverters" + and self.coordinator.data.get("inverters_production") is not None + ): + value = self.coordinator.data.get("inverters_production").get( + self._serial_number + )[1] + return {"last_reported": value} return None - - async def async_update(self): - """Get the energy production data from the Enphase Envoy.""" - if self._type != "inverters": - _state = await getattr(self._envoy_reader, self._type)() - if isinstance(_state, int): - self._state = _state - else: - _LOGGER.error(_state) - self._state = None - - elif self._type == "inverters": - try: - inverters = await (self._envoy_reader.inverters_production()) - except requests.exceptions.HTTPError: - _LOGGER.warning( - "Authentication for Inverter data failed during update: %s", - self._envoy_reader.host, - ) - - if isinstance(inverters, dict): - serial_number = self._name.split(" ")[2] - self._state = inverters[serial_number][0] - self._last_reported = inverters[serial_number][1] - else: - self._state = None diff --git a/requirements_all.txt b/requirements_all.txt index 5bb2f37d9a..9504cba0d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -556,7 +556,7 @@ env_canada==0.2.4 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.17.3 +envoy_reader==0.18.2 # homeassistant.components.season ephem==3.7.7.0 From 5164a18d53146ce1d26efc20cd99008e80e0def8 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 28 Dec 2020 17:13:11 -0800 Subject: [PATCH 244/302] Bump version to fix returned data for old firmware (#44600) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 87491a7254..9e9760560d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,7 +2,7 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.18.2"], + "requirements": ["envoy_reader==0.18.3"], "codeowners": [ "@gtdiehl" ] diff --git a/requirements_all.txt b/requirements_all.txt index 9504cba0d5..6fc710ab28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -556,7 +556,7 @@ env_canada==0.2.4 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.18.2 +envoy_reader==0.18.3 # homeassistant.components.season ephem==3.7.7.0 From e5f31665b104dee42e2ec2fc282b3c75e3b40ed5 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 29 Dec 2020 11:06:12 +0100 Subject: [PATCH 245/302] Add Config Flow to bmw_connected_drive (#39585) * Add Config Flow to bmw_connected_drive * Fix checks for bmw_connected_drive * Adjust code as requested * Clean .coveragerc after merge * Use references for config flow * Fix execute_service check against allowed accounts * Adjust translation as username can be email or phone no * Add BMWConnectedDriveBaseEntity mixin, remove unnecessary type casts * Use BaseEntity correctly, fix pylint error * Bump bimmer_connected to 0.7.13 * Adjustments for review * Fix pylint * Fix loading notify, move vin to entity attrs * Remove vin from device registry * Remove commented-out code * Show tracker warning only if vehicle (currently) doesn't support location * Remove unnecessary return values & other small adjustments * Move original hass_config to own domain in hass.data * Move entries to separate dict in hass.data * Remove invalid_auth exception handling & test as it cannot happen Co-authored-by: rikroe --- .coveragerc | 7 +- .../bmw_connected_drive/__init__.py | 276 +++++++++++++++--- .../bmw_connected_drive/binary_sensor.py | 83 ++---- .../bmw_connected_drive/config_flow.py | 119 ++++++++ .../components/bmw_connected_drive/const.py | 10 + .../bmw_connected_drive/device_tracker.py | 104 ++++--- .../components/bmw_connected_drive/lock.py | 64 ++-- .../bmw_connected_drive/manifest.json | 3 +- .../components/bmw_connected_drive/notify.py | 3 +- .../components/bmw_connected_drive/sensor.py | 65 ++--- .../bmw_connected_drive/strings.json | 30 ++ .../bmw_connected_drive/translations/en.json | 31 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../bmw_connected_drive/__init__.py | 1 + .../bmw_connected_drive/test_config_flow.py | 153 ++++++++++ 16 files changed, 736 insertions(+), 217 deletions(-) create mode 100644 homeassistant/components/bmw_connected_drive/config_flow.py create mode 100644 homeassistant/components/bmw_connected_drive/strings.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/en.json create mode 100644 tests/components/bmw_connected_drive/__init__.py create mode 100644 tests/components/bmw_connected_drive/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c16b6ecb98..9f2fdc8071 100644 --- a/.coveragerc +++ b/.coveragerc @@ -100,7 +100,12 @@ omit = homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmp280/sensor.py - homeassistant/components/bmw_connected_drive/* + homeassistant/components/bmw_connected_drive/__init__.py + homeassistant/components/bmw_connected_drive/binary_sensor.py + homeassistant/components/bmw_connected_drive/device_tracker.py + homeassistant/components/bmw_connected_drive/lock.py + homeassistant/components/bmw_connected_drive/notify.py + homeassistant/components/bmw_connected_drive/sensor.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c72d1ce40f..e9f6a0d7f6 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,29 +1,50 @@ """Reads vehicle status from BMW connected drive portal.""" +import asyncio import logging from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_utc_time_change +from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from .const import ( + ATTRIBUTION, + CONF_ACCOUNT, + CONF_ALLOWED_REGIONS, + CONF_READ_ONLY, + CONF_REGION, + CONF_USE_LOCATION, + DATA_ENTRIES, + DATA_HASS_CONFIG, +) + _LOGGER = logging.getLogger(__name__) DOMAIN = "bmw_connected_drive" -CONF_REGION = "region" -CONF_READ_ONLY = "read_only" ATTR_VIN = "vin" ACCOUNT_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_REGION): vol.Any("north_america", "china", "rest_of_world"), - vol.Optional(CONF_READ_ONLY, default=False): cv.boolean, + vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), + vol.Optional(CONF_READ_ONLY): cv.boolean, } ) @@ -31,8 +52,12 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLO SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) +DEFAULT_OPTIONS = { + CONF_READ_ONLY: False, + CONF_USE_LOCATION: False, +} -BMW_COMPONENTS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] +BMW_PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] UPDATE_INTERVAL = 5 # in minutes SERVICE_UPDATE_STATE = "update_state" @@ -44,49 +69,162 @@ _SERVICE_MAP = { "find_vehicle": "trigger_remote_vehicle_finder", } +UNDO_UPDATE_LISTENER = "undo_update_listener" -def setup(hass, config: dict): - """Set up the BMW connected drive components.""" - accounts = [] - for name, account_config in config[DOMAIN].items(): - accounts.append(setup_account(account_config, hass, name)) - hass.data[DOMAIN] = accounts +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the BMW Connected Drive component from configuration.yaml.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][DATA_HASS_CONFIG] = config - def _update_all(call) -> None: - """Update all BMW accounts.""" - for cd_account in hass.data[DOMAIN]: - cd_account.update() - - # Service to manually trigger updates for all accounts. - hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all) - - _update_all(None) - - for component in BMW_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + if DOMAIN in config: + for entry_config in config[DOMAIN].values(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config + ) + ) return True -def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAccount": +@callback +def _async_migrate_options_from_data_if_missing(hass, entry): + data = dict(entry.data) + options = dict(entry.options) + + if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS): + options = dict(DEFAULT_OPTIONS, **options) + options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False) + + hass.config_entries.async_update_entry(entry, data=data, options=options) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up BMW Connected Drive from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(DATA_ENTRIES, {}) + + _async_migrate_options_from_data_if_missing(hass, entry) + + try: + account = await hass.async_add_executor_job( + setup_account, entry, hass, entry.data[CONF_USERNAME] + ) + except OSError as ex: + raise ConfigEntryNotReady from ex + + async def _async_update_all(service_call=None): + """Update all BMW accounts.""" + await hass.async_add_executor_job(_update_all) + + def _update_all() -> None: + """Update all BMW accounts.""" + for entry in hass.data[DOMAIN][DATA_ENTRIES].values(): + entry[CONF_ACCOUNT].update() + + # Add update listener for config entry changes (options) + undo_listener = entry.add_update_listener(update_listener) + + hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id] = { + CONF_ACCOUNT: account, + UNDO_UPDATE_LISTENER: undo_listener, + } + + # Service to manually trigger updates for all accounts. + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, _async_update_all) + + await _async_update_all() + + for platform in BMW_PLATFORMS: + if platform != NOTIFY_DOMAIN: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, + NOTIFY_DOMAIN, + DOMAIN, + {CONF_NAME: DOMAIN}, + hass.data[DOMAIN][DATA_HASS_CONFIG], + ) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in BMW_PLATFORMS + if component != NOTIFY_DOMAIN + ] + ) + ) + + # Only remove services if it is the last account and not read only + if ( + len(hass.data[DOMAIN][DATA_ENTRIES]) == 1 + and not hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][CONF_ACCOUNT].read_only + ): + services = list(_SERVICE_MAP) + [SERVICE_UPDATE_STATE] + for service in services: + hass.services.async_remove(DOMAIN, service) + + for vehicle in hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][ + CONF_ACCOUNT + ].account.vehicles: + hass.services.async_remove(NOTIFY_DOMAIN, slugify(f"{DOMAIN}_{vehicle.name}")) + + if unload_ok: + hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN][DATA_ENTRIES].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass, config_entry): + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +def setup_account(entry: ConfigEntry, hass, name: str) -> "BMWConnectedDriveAccount": """Set up a new BMWConnectedDriveAccount based on the config.""" - username = account_config[CONF_USERNAME] - password = account_config[CONF_PASSWORD] - region = account_config[CONF_REGION] - read_only = account_config[CONF_READ_ONLY] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + region = entry.data[CONF_REGION] + read_only = entry.options[CONF_READ_ONLY] + use_location = entry.options[CONF_USE_LOCATION] _LOGGER.debug("Adding new account %s", name) - cd_account = BMWConnectedDriveAccount(username, password, region, name, read_only) + + pos = ( + (hass.config.latitude, hass.config.longitude) if use_location else (None, None) + ) + cd_account = BMWConnectedDriveAccount( + username, password, region, name, read_only, *pos + ) def execute_service(call): - """Execute a service for a vehicle. - - This must be a member function as we need access to the cd_account - object here. - """ + """Execute a service for a vehicle.""" vin = call.data[ATTR_VIN] - vehicle = cd_account.account.get_vehicle(vin) + vehicle = None + # Double check for read_only accounts as another account could create the services + for entry_data in [ + e + for e in hass.data[DOMAIN][DATA_ENTRIES].values() + if not e[CONF_ACCOUNT].read_only + ]: + vehicle = entry_data[CONF_ACCOUNT].account.get_vehicle(vin) + if vehicle: + break if not vehicle: _LOGGER.error("Could not find a vehicle for VIN %s", vin) return @@ -111,6 +249,9 @@ def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAc second=now.second, ) + # Initialize + cd_account.update() + return cd_account @@ -118,7 +259,14 @@ class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" def __init__( - self, username: str, password: str, region_str: str, name: str, read_only + self, + username: str, + password: str, + region_str: str, + name: str, + read_only: bool, + lat=None, + lon=None, ) -> None: """Initialize account.""" region = get_region_from_name(region_str) @@ -128,6 +276,12 @@ class BMWConnectedDriveAccount: self.name = name self._update_listeners = [] + # Set observer position once for older cars to be in range for + # GPS position (pre-7/2014, <2km) and get new data from API + if lat and lon: + self.account.set_observer_position(lat, lon) + self.account.update_vehicle_states() + def update(self, *_): """Update the state of all vehicles. @@ -152,3 +306,51 @@ class BMWConnectedDriveAccount: def add_update_listener(self, listener): """Add a listener for update notifications.""" self._update_listeners.append(listener) + + +class BMWConnectedDriveBaseEntity(Entity): + """Common base for BMW entities.""" + + def __init__(self, account, vehicle): + """Initialize sensor.""" + self._account = account + self._vehicle = vehicle + self._attrs = { + "car": self._vehicle.name, + "vin": self._vehicle.vin, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + @property + def device_info(self) -> dict: + """Return info for device registry.""" + return { + "identifiers": {(DOMAIN, self._vehicle.vin)}, + "name": f'{self._vehicle.attributes.get("brand")} {self._vehicle.name}', + "model": self._vehicle.name, + "manufacturer": self._vehicle.attributes.get("brand"), + } + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return self._attrs + + @property + def should_poll(self): + """Do not poll this class. + + Updates are triggered from BMWConnectedDriveAccount. + """ + return False + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 31ef2dacf3..cad5426d54 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,10 +9,10 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS +from homeassistant.const import LENGTH_KILOMETERS -from . import DOMAIN as BMW_DOMAIN -from .const import ATTRIBUTION +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) @@ -41,41 +41,40 @@ SENSOR_TYPES_ELEC = { SENSOR_TYPES_ELEC.update(SENSOR_TYPES) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW sensors.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - devices = [] - for account in accounts: - for vehicle in account.account.vehicles: - if vehicle.has_hv_battery: - _LOGGER.debug("BMW with a high voltage battery") - for key, value in sorted(SENSOR_TYPES_ELEC.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - devices.append(device) - elif vehicle.has_internal_combustion_engine: - _LOGGER.debug("BMW with an internal combustion engine") - for key, value in sorted(SENSOR_TYPES.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - devices.append(device) - add_entities(devices, True) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive binary sensors from config entry.""" + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + for vehicle in account.account.vehicles: + if vehicle.has_hv_battery: + _LOGGER.debug("BMW with a high voltage battery") + for key, value in sorted(SENSOR_TYPES_ELEC.items()): + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + entities.append(device) + elif vehicle.has_internal_combustion_engine: + _LOGGER.debug("BMW with an internal combustion engine") + for key, value in sorted(SENSOR_TYPES.items()): + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + entities.append(device) + async_add_entities(entities, True) -class BMWConnectedDriveSensor(BinarySensorEntity): +class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" def __init__( self, account, vehicle, attribute: str, sensor_name, device_class, icon ): """Initialize sensor.""" - self._account = account - self._vehicle = vehicle + super().__init__(account, vehicle) + self._attribute = attribute self._name = f"{self._vehicle.name} {self._attribute}" self._unique_id = f"{self._vehicle.vin}-{self._attribute}" @@ -84,14 +83,6 @@ class BMWConnectedDriveSensor(BinarySensorEntity): self._icon = icon self._state = None - @property - def should_poll(self) -> bool: - """Return False. - - Data update is triggered from BMWConnectedDriveEntity. - """ - return False - @property def unique_id(self): """Return the unique ID of the binary sensor.""" @@ -121,10 +112,7 @@ class BMWConnectedDriveSensor(BinarySensorEntity): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state - result = { - "car": self._vehicle.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } + result = self._attrs.copy() if self._attribute == "lids": for lid in vehicle_state.lids: @@ -205,14 +193,3 @@ class BMWConnectedDriveSensor(BinarySensorEntity): f"{service_type} distance" ] = f"{distance} {self.hass.config.units.length_unit}" return result - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py new file mode 100644 index 0000000000..a6081d5ccc --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -0,0 +1,119 @@ +"""Config flow for BMW ConnectedDrive integration.""" +import logging + +from bimmer_connected.account import ConnectedDriveAccount +from bimmer_connected.country_selector import get_region_from_name +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME +from homeassistant.core import callback + +from . import DOMAIN # pylint: disable=unused-import +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_REGION, CONF_USE_LOCATION + +_LOGGER = logging.getLogger(__name__) + + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), + } +) + + +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. + """ + try: + await hass.async_add_executor_job( + ConnectedDriveAccount, + data[CONF_USERNAME], + data[CONF_PASSWORD], + get_region_from_name(data[CONF_REGION]), + ) + except OSError as ex: + raise CannotConnect from ex + + # Return info that you want to store in the config entry. + return {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"} + + +class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for BMW ConnectedDrive.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}" + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + info = None + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + + if info: + 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 + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return a BWM ConnectedDrive option flow.""" + return BMWConnectedDriveOptionsFlow(config_entry) + + +class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): + """Handle a option flow for BMW ConnectedDrive.""" + + def __init__(self, config_entry): + """Initialize BMW ConnectedDrive option flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_account_options() + + async def async_step_account_options(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + return self.async_show_form( + step_id="account_options", + data_schema=vol.Schema( + { + vol.Optional( + CONF_READ_ONLY, + default=self.config_entry.options.get(CONF_READ_ONLY, False), + ): bool, + vol.Optional( + CONF_USE_LOCATION, + default=self.config_entry.options.get(CONF_USE_LOCATION, False), + ): bool, + } + ), + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index d1a44b5e5c..65dc7fde59 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,2 +1,12 @@ """Const file for the BMW Connected Drive integration.""" ATTRIBUTION = "Data provided by BMW Connected Drive" + +CONF_REGION = "region" +CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] +CONF_READ_ONLY = "read_only" +CONF_USE_LOCATION = "use_location" + +CONF_ACCOUNT = "account" + +DATA_HASS_CONFIG = "hass_config" +DATA_ENTRIES = "entries" diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index fa732b64e7..7f069e741b 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -1,51 +1,83 @@ """Device tracker for BMW Connected Drive vehicles.""" import logging -from homeassistant.util import slugify +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity -from . import DOMAIN as BMW_DOMAIN +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the BMW tracker.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - for account in accounts: - for vehicle in account.account.vehicles: - tracker = BMWDeviceTracker(see, vehicle) - account.add_update_listener(tracker.update) - tracker.update() - return True +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive tracker from config entry.""" + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + for vehicle in account.account.vehicles: + entities.append(BMWDeviceTracker(account, vehicle)) + if not vehicle.state.is_vehicle_tracking_enabled: + _LOGGER.info( + "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", + vehicle.name, + vehicle.vin, + ) + async_add_entities(entities, True) -class BMWDeviceTracker: +class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): """BMW Connected Drive device tracker.""" - def __init__(self, see, vehicle): + def __init__(self, account, vehicle): """Initialize the Tracker.""" - self._see = see - self.vehicle = vehicle + super().__init__(account, vehicle) - def update(self) -> None: - """Update the device info. - - Only update the state in Home Assistant if tracking in - the car is enabled. - """ - dev_id = slugify(self.vehicle.name) - - if not self.vehicle.state.is_vehicle_tracking_enabled: - _LOGGER.debug("Tracking is disabled for vehicle %s", dev_id) - return - - _LOGGER.debug("Updating %s", dev_id) - attrs = {"vin": self.vehicle.vin} - self._see( - dev_id=dev_id, - host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, - attributes=attrs, - icon="mdi:car", + self._unique_id = vehicle.vin + self._location = ( + vehicle.state.gps_position if vehicle.state.gps_position else (None, None) + ) + self._name = vehicle.name + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._location[0] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._location[1] + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:car" + + @property + def force_update(self): + """All updates do not need to be written to the state machine.""" + return False + + def update(self): + """Update state of the decvice tracker.""" + self._location = ( + self._vehicle.state.gps_position + if self._vehicle.state.is_vehicle_tracking_enabled + else (None, None) ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index d30f1702ae..0d281e78f1 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -4,35 +4,34 @@ import logging from bimmer_connected.state import LockState from homeassistant.components.lock import LockEntity -from homeassistant.const import ATTR_ATTRIBUTION, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from . import DOMAIN as BMW_DOMAIN -from .const import ATTRIBUTION +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES DOOR_LOCK_STATE = "door_lock_state" _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW Connected Drive lock.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - devices = [] - for account in accounts: - if not account.read_only: - for vehicle in account.account.vehicles: - device = BMWLock(account, vehicle, "lock", "BMW lock") - devices.append(device) - add_entities(devices, True) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive binary sensors from config entry.""" + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + if not account.read_only: + for vehicle in account.account.vehicles: + device = BMWLock(account, vehicle, "lock", "BMW lock") + entities.append(device) + async_add_entities(entities, True) -class BMWLock(LockEntity): +class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): """Representation of a BMW vehicle lock.""" def __init__(self, account, vehicle, attribute: str, sensor_name): """Initialize the lock.""" - self._account = account - self._vehicle = vehicle + super().__init__(account, vehicle) + self._attribute = attribute self._name = f"{self._vehicle.name} {self._attribute}" self._unique_id = f"{self._vehicle.vin}-{self._attribute}" @@ -42,14 +41,6 @@ class BMWLock(LockEntity): DOOR_LOCK_STATE in self._vehicle.available_attributes ) - @property - def should_poll(self): - """Do not poll this class. - - Updates are triggered from BMWConnectedDriveAccount. - """ - return False - @property def unique_id(self): """Return the unique ID of the lock.""" @@ -64,10 +55,8 @@ class BMWLock(LockEntity): def device_state_attributes(self): """Return the state attributes of the lock.""" vehicle_state = self._vehicle.state - result = { - "car": self._vehicle.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } + result = self._attrs.copy() + if self.door_lock_state_available: result["door_lock_state"] = vehicle_state.door_lock_state.value result["last_update_reason"] = vehicle_state.last_update_reason @@ -76,7 +65,11 @@ class BMWLock(LockEntity): @property def is_locked(self): """Return true if lock is locked.""" - return self._state == STATE_LOCKED + if self.door_lock_state_available: + result = self._state == STATE_LOCKED + else: + result = None + return result def lock(self, **kwargs): """Lock the car.""" @@ -107,14 +100,3 @@ class BMWLock(LockEntity): if vehicle_state.door_lock_state in [LockState.LOCKED, LockState.SECURED] else STATE_UNLOCKED ) - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index cb17459e10..5bce904e1c 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -3,5 +3,6 @@ "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": ["bimmer_connected==0.7.13"], - "codeowners": ["@gerard33", "@rikroe"] + "codeowners": ["@gerard33", "@rikroe"], + "config_flow": true } diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 9cf2bca2df..3fd40f3801 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -11,6 +11,7 @@ from homeassistant.components.notify import ( from homeassistant.const import ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, ATTR_NAME from . import DOMAIN as BMW_DOMAIN +from .const import CONF_ACCOUNT, DATA_ENTRIES ATTR_LAT = "lat" ATTR_LOCATION_ATTRIBUTES = ["street", "city", "postal_code", "country"] @@ -23,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) def get_service(hass, config, discovery_info=None): """Get the BMW notification service.""" - accounts = hass.data[BMW_DOMAIN] + accounts = [e[CONF_ACCOUNT] for e in hass.data[BMW_DOMAIN][DATA_ENTRIES].values()] _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) svc = BMWNotificationService() svc.setup(accounts) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 4668b1da6e..480aac34eb 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -4,7 +4,6 @@ import logging from bimmer_connected.state import ChargingState from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, @@ -16,8 +15,8 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from . import DOMAIN as BMW_DOMAIN -from .const import ATTRIBUTION +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) @@ -48,48 +47,39 @@ ATTR_TO_HA_IMPERIAL = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW sensors.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive sensors from config entry.""" if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: attribute_info = ATTR_TO_HA_IMPERIAL else: attribute_info = ATTR_TO_HA_METRIC - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - devices = [] - for account in accounts: - for vehicle in account.account.vehicles: - for attribute_name in vehicle.drive_train_attributes: - if attribute_name in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - devices.append(device) - add_entities(devices, True) + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + for vehicle in account.account.vehicles: + for attribute_name in vehicle.drive_train_attributes: + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + entities.append(device) + async_add_entities(entities, True) -class BMWConnectedDriveSensor(Entity): +class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, Entity): """Representation of a BMW vehicle sensor.""" def __init__(self, account, vehicle, attribute: str, attribute_info): """Initialize BMW vehicle sensor.""" - self._vehicle = vehicle - self._account = account + super().__init__(account, vehicle) + self._attribute = attribute self._state = None self._name = f"{self._vehicle.name} {self._attribute}" self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attribute_info = attribute_info - @property - def should_poll(self) -> bool: - """Return False. - - Data update is triggered from BMWConnectedDriveEntity. - """ - return False - @property def unique_id(self): """Return the unique ID of the sensor.""" @@ -128,14 +118,6 @@ class BMWConnectedDriveSensor(Entity): unit = self._attribute_info.get(self._attribute, [None, None])[1] return unit - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - return { - "car": self._vehicle.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) @@ -152,14 +134,3 @@ class BMWConnectedDriveSensor(Entity): self._state = round(value_converted) else: self._state = getattr(vehicle_state, self._attribute) - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json new file mode 100644 index 0000000000..c0c45b814a --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "region": "ConnectedDrive Region" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", + "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + } + } + } + } +} diff --git a/homeassistant/components/bmw_connected_drive/translations/en.json b/homeassistant/components/bmw_connected_drive/translations/en.json new file mode 100644 index 0000000000..f194c8a344 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "password": "Password", + "read_only": "Read-only", + "region": "ConnectedDrive Region", + "username": "Username" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", + "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 11a0b51764..9e204e91da 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ FLOWS = [ "azure_devops", "blebox", "blink", + "bmw_connected_drive", "bond", "braviatv", "broadlink", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ff9bd8c97..73503ac0e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,6 +188,9 @@ base36==0.1.1 # homeassistant.components.zha bellows==0.21.0 +# homeassistant.components.bmw_connected_drive +bimmer_connected==0.7.13 + # homeassistant.components.blebox blebox_uniapi==1.3.2 diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py new file mode 100644 index 0000000000..e1243fe2c0 --- /dev/null +++ b/tests/components/bmw_connected_drive/__init__.py @@ -0,0 +1 @@ +"""Tests for the for the BMW Connected Drive integration.""" diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py new file mode 100644 index 0000000000..ae32feec7b --- /dev/null +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -0,0 +1,153 @@ +"""Test the for the BMW Connected Drive config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN +from homeassistant.components.bmw_connected_drive.const import ( + CONF_READ_ONLY, + CONF_REGION, + CONF_USE_LOCATION, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = { + CONF_USERNAME: "user@domain.com", + CONF_PASSWORD: "p4ssw0rd", + CONF_REGION: "rest_of_world", +} +FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() +FIXTURE_IMPORT_ENTRY = FIXTURE_USER_INPUT.copy() + +FIXTURE_CONFIG_ENTRY = { + "entry_id": "1", + "domain": DOMAIN, + "title": FIXTURE_USER_INPUT[CONF_USERNAME], + "data": { + CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], + CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], + CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], + }, + "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + "system_options": {"disable_new_entities": False}, + "source": "user", + "connection_class": config_entries.CONN_CLASS_CLOUD_POLL, + "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", +} + + +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": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_connection_error(hass): + """Test we show user form on BMW connected drive connection error.""" + + def _mock_get_oauth_token(*args, **kwargs): + pass + + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_oauth_token", + side_effect=OSError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_full_user_flow_implementation(hass): + """Test registering an integration and finishing flow works.""" + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + return_value=[], + ), patch( + "homeassistant.components.bmw_connected_drive.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] + assert result2["data"] == FIXTURE_COMPLETE_ENTRY + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_config_flow_implementation(hass): + """Test registering an integration and finishing flow works.""" + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + return_value=[], + ), patch( + "homeassistant.components.bmw_connected_drive.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.bmw_connected_drive.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=FIXTURE_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FIXTURE_IMPORT_ENTRY[CONF_USERNAME] + assert result["data"] == FIXTURE_IMPORT_ENTRY + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow_implementation(hass): + """Test config flow options.""" + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + return_value=[], + ), patch( + "homeassistant.components.bmw_connected_drive.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "account_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_READ_ONLY: False, + CONF_USE_LOCATION: False, + } + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 598202da07040c428676a28dc01b15eda5c2fcb1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 29 Dec 2020 12:11:08 +0100 Subject: [PATCH 246/302] Simplify motion blinds push callback (#44579) --- homeassistant/components/motion_blinds/cover.py | 8 +------- homeassistant/components/motion_blinds/sensor.py | 15 ++------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 06c1f1d273..2b42c1be66 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -16,7 +16,6 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -170,14 +169,9 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Return if the cover is closed or not.""" return self._blind.position == 100 - @callback - def _push_callback(self): - """Update entity state when a push has been received.""" - self.schedule_update_ha_state(force_refresh=False) - async def async_added_to_hass(self): """Subscribe to multicast pushes and register signal handler.""" - self._blind.Register_callback(self.unique_id, self._push_callback) + self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) ) diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 0c31ca070c..dd637696e7 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -9,7 +9,6 @@ from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) -from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -97,14 +96,9 @@ class MotionBatterySensor(CoordinatorEntity, Entity): """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} - @callback - def push_callback(self): - """Update entity state when a push has been received.""" - self.schedule_update_ha_state(force_refresh=False) - async def async_added_to_hass(self): """Subscribe to multicast pushes.""" - self._blind.Register_callback(self.unique_id, self.push_callback) + self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() async def async_will_remove_from_hass(self): @@ -206,14 +200,9 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity): """Return the state of the sensor.""" return self._device.RSSI - @callback - def push_callback(self): - """Update entity state when a push has been received.""" - self.schedule_update_ha_state(force_refresh=False) - async def async_added_to_hass(self): """Subscribe to multicast pushes.""" - self._device.Register_callback(self.unique_id, self.push_callback) + self._device.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() async def async_will_remove_from_hass(self): From c756457aa17ef425e2dc6caa15b639476b642225 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 29 Dec 2020 14:40:52 +0200 Subject: [PATCH 247/302] Fix typo in sensor names (#44598) Fixes home-assistant/core#44464 --- homeassistant/components/jewish_calendar/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 70d55a74b9..d1474c3cf5 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -26,8 +26,8 @@ SENSOR_TYPES = { "talit": ["Talit and Tefillin", "mdi:calendar-clock"], "gra_end_shma": ['Latest time for Shma Gr"a', "mdi:calendar-clock"], "mga_end_shma": ['Latest time for Shma MG"A', "mdi:calendar-clock"], - "gra_end_tfila": ['Latest time for Tefilla MG"A', "mdi:calendar-clock"], - "mga_end_tfila": ['Latest time for Tefilla Gr"a', "mdi:calendar-clock"], + "gra_end_tfila": ['Latest time for Tefilla Gr"a', "mdi:calendar-clock"], + "mga_end_tfila": ['Latest time for Tefilla MG"A', "mdi:calendar-clock"], "big_mincha": ["Mincha Gedola", "mdi:calendar-clock"], "small_mincha": ["Mincha Ketana", "mdi:calendar-clock"], "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], From 4905be0c4004e8c3cfd352ac66fafeb97d245300 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Dec 2020 02:54:24 -1000 Subject: [PATCH 248/302] Move HomeKit autostart to advanced options flow (#44599) --- .../components/homekit/config_flow.py | 1 - homeassistant/components/homekit/strings.json | 3 +-- tests/components/homekit/test_config_flow.py | 27 ++++++++++--------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 65b70a8463..9d50e62fcd 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -128,7 +128,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS setup_schema = vol.Schema( { - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool, vol.Required( CONF_INCLUDE_DOMAINS, default=default_domains ): cv.multi_select(SUPPORTED_DOMAINS), diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 5270ac6970..3f12eca0f5 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -30,7 +30,7 @@ }, "advanced": { "data": { - "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", "safe_mode": "Safe Mode (enable only if pairing fails)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", @@ -42,7 +42,6 @@ "step": { "user": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", "include_domains": "Domains to include" }, "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 60dc293c4f..59d6597706 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1,4 +1,6 @@ """Test the HomeKit config flow.""" +import pytest + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.homekit.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT @@ -25,7 +27,6 @@ def _mock_config_entry_with_options_populated(): ], "exclude_entities": ["climate.front_gate"], }, - "auto_start": False, "safe_mode": False, }, ) @@ -46,7 +47,7 @@ async def test_user_form(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"auto_start": True, "include_domains": ["light"]}, + {"include_domains": ["light"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -68,7 +69,6 @@ async def test_user_form(hass): assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { - "auto_start": True, "filter": { "exclude_domains": [], "exclude_entities": [], @@ -123,7 +123,8 @@ async def test_import(hass): assert len(mock_setup_entry.mock_calls) == 2 -async def test_options_flow_exclude_mode_advanced(hass): +@pytest.mark.parametrize("auto_start", [True, False]) +async def test_options_flow_exclude_mode_advanced(auto_start, hass): """Test config flow options in exclude mode with advanced options.""" config_entry = _mock_config_entry_with_options_populated() @@ -157,12 +158,12 @@ async def test_options_flow_exclude_mode_advanced(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"auto_start": True, "safe_mode": True}, + user_input={"auto_start": auto_start, "safe_mode": True}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, + "auto_start": auto_start, "mode": "bridge", "filter": { "exclude_domains": [], @@ -213,7 +214,7 @@ async def test_options_flow_exclude_mode_basic(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -266,7 +267,7 @@ async def test_options_flow_include_mode_basic(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -332,7 +333,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -387,7 +388,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -454,7 +455,7 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -509,7 +510,7 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -603,7 +604,7 @@ async def test_options_flow_include_mode_basic_accessory(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "accessory", "filter": { "exclude_domains": [], From 24f6f59eb4fe5841b6528b838a4776bf788c1dd7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 29 Dec 2020 16:21:51 +0100 Subject: [PATCH 249/302] Use entity service for motion blinds (#44611) * Simplify motion blinds service * Switch to using entity service --- .../components/motion_blinds/__init__.py | 41 +------------------ .../components/motion_blinds/cover.py | 36 ++++++++-------- 2 files changed, 18 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 2f087cbe52..e10f1655d2 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -5,65 +5,28 @@ import logging from socket import timeout from motionblinds import MotionMulticast -import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_API_KEY, - CONF_HOST, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( - ATTR_ABSOLUTE_POSITION, - ATTR_WIDTH, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, MANUFACTURER, MOTION_PLATFORMS, - SERVICE_SET_ABSOLUTE_POSITION, ) from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) -CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) - -SET_ABSOLUTE_POSITION_SCHEMA = CALL_SCHEMA.extend( - { - vol.Required(ATTR_ABSOLUTE_POSITION): vol.All( - cv.positive_int, vol.Range(max=100) - ), - vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), - } -) - -SERVICE_TO_METHOD = { - SERVICE_SET_ABSOLUTE_POSITION: { - "schema": SET_ABSOLUTE_POSITION_SCHEMA, - } -} - def setup(hass: core.HomeAssistant, config: dict): """Set up the Motion Blinds component.""" - - def service_handler(service): - data = service.data.copy() - data["method"] = service.service - dispatcher_send(hass, DOMAIN, data) - - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.register(DOMAIN, service, service_handler, schema=schema) - return True diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 2b42c1be66..3087401c3a 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -3,6 +3,7 @@ import logging from motionblinds import BlindType +import voluptuous as vol from homeassistant.components.cover import ( ATTR_POSITION, @@ -15,8 +16,7 @@ from homeassistant.components.cover import ( DEVICE_CLASS_SHUTTER, CoverEntity, ) -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -26,6 +26,7 @@ from .const import ( KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER, + SERVICE_SET_ABSOLUTE_POSITION, ) _LOGGER = logging.getLogger(__name__) @@ -57,6 +58,12 @@ TDBU_DEVICE_MAP = { } +SET_ABSOLUTE_POSITION_SCHEMA = { + vol.Required(ATTR_ABSOLUTE_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), + vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), +} + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Motion Blind from a config entry.""" entities = [] @@ -108,6 +115,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_SET_ABSOLUTE_POSITION, + SET_ABSOLUTE_POSITION_SCHEMA, + SERVICE_SET_ABSOLUTE_POSITION, + ) + class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Representation of a Motion Blind Device.""" @@ -172,26 +186,8 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): async def async_added_to_hass(self): """Subscribe to multicast pushes and register signal handler.""" self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) - self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) - ) await super().async_added_to_hass() - 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 - for key, value in data.items() - if key not in ["entity_id", "method"] - } - getattr(self, data["method"])(**params) - async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" self._blind.Remove_callback(self.unique_id) From 85d89c16abd9ebb57d2d4253cc255a6130a5de9f Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Tue, 29 Dec 2020 08:48:36 -0800 Subject: [PATCH 250/302] Bump skybellpy to 0.6.3 (#44619) --- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 1b97b80095..4d621d18fa 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -2,6 +2,6 @@ "domain": "skybell", "name": "SkyBell", "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["skybellpy==0.6.1"], + "requirements": ["skybellpy==0.6.3"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 6fc710ab28..ea6b464f66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2022,7 +2022,7 @@ simplisafe-python==9.6.2 sisyphus-control==3.0 # homeassistant.components.skybell -skybellpy==0.6.1 +skybellpy==0.6.3 # homeassistant.components.slack slackclient==2.5.0 From e287160f72d3f6f16ffa02ba3b158ac595760979 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 29 Dec 2020 20:13:31 +0100 Subject: [PATCH 251/302] Add discovery to Motion Blinds (#44615) * Add discovery to Motion Blinds * Update test_config_flow.py * ommit keys() Co-authored-by: Allen Porter * use _ to indicate private variables * disregard changes to en.json * remove unused errors * clearify multicast=None * improve tests * make self._key a local variable * fix styling Co-authored-by: Allen Porter --- .../components/motion_blinds/config_flow.py | 69 +++++-- .../components/motion_blinds/strings.json | 21 ++- .../motion_blinds/test_config_flow.py | 175 +++++++++++++++++- 3 files changed, 244 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 497f11760f..cb85b45e0e 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure Motion Blinds using their WLAN API.""" import logging +from motionblinds import MotionDiscovery import voluptuous as vol from homeassistant import config_entries @@ -15,7 +16,12 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Optional(CONF_HOST): str, + } +) + +CONFIG_SETTINGS = vol.Schema( + { vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)), } ) @@ -29,35 +35,64 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Motion Blinds flow.""" - self.host = None - self.key = None + self._host = None + self._ips = [] async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - self.host = user_input[CONF_HOST] - self.key = user_input[CONF_API_KEY] - return await self.async_step_connect() + self._host = user_input.get(CONF_HOST) + + if self._host is not None: + return await self.async_step_connect() + + # Use MotionGateway discovery + discover_class = MotionDiscovery() + gateways = await self.hass.async_add_executor_job(discover_class.discover) + self._ips = list(gateways) + + if len(self._ips) == 1: + self._host = self._ips[0] + return await self.async_step_connect() + + if len(self._ips) > 1: + return await self.async_step_select() + + errors["base"] = "discovery_error" return self.async_show_form( step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) + async def async_step_select(self, user_input=None): + """Handle multiple motion gateways found.""" + if user_input is not None: + self._host = user_input["select_ip"] + return await self.async_step_connect() + + select_schema = vol.Schema({vol.Required("select_ip"): vol.In(self._ips)}) + + return self.async_show_form(step_id="select", data_schema=select_schema) + async def async_step_connect(self, user_input=None): """Connect to the Motion Gateway.""" + if user_input is not None: + key = user_input[CONF_API_KEY] - connect_gateway_class = ConnectMotionGateway(self.hass, None) - if not await connect_gateway_class.async_connect_gateway(self.host, self.key): - return self.async_abort(reason="connection_error") - motion_gateway = connect_gateway_class.gateway_device + connect_gateway_class = ConnectMotionGateway(self.hass, multicast=None) + if not await connect_gateway_class.async_connect_gateway(self._host, key): + return self.async_abort(reason="connection_error") + motion_gateway = connect_gateway_class.gateway_device - mac_address = motion_gateway.mac + mac_address = motion_gateway.mac - await self.async_set_unique_id(mac_address) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured() - return self.async_create_entry( - title=DEFAULT_GATEWAY_NAME, - data={CONF_HOST: self.host, CONF_API_KEY: self.key}, - ) + return self.async_create_entry( + title=DEFAULT_GATEWAY_NAME, + data={CONF_HOST: self._host, CONF_API_KEY: key}, + ) + + return self.async_show_form(step_id="connect", data_schema=CONFIG_SETTINGS) diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index d9c8a4099a..d922923d47 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -3,14 +3,30 @@ "flow_title": "Motion Blinds", "step": { "user": { + "title": "Motion Blinds", + "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", + "data": { + "host": "[%key:common::config_flow::data::ip%]" + } + }, + "connect": { "title": "Motion Blinds", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "data": { - "host": "[%key:common::config_flow::data::ip%]", "api_key": "[%key:common::config_flow::data::api_key%]" } + }, + "select": { + "title": "Select the Motion Gateway that you wish to connect", + "description": "Run the setup again if you want to connect additional Motion Gateways", + "data": { + "select_ip": "[%key:common::config_flow::data::ip%]" + } } }, + "error": { + "discovery_error": "Failed to discover a Motion Gateway" + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", @@ -18,3 +34,6 @@ } } } + + + diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 4514beda8c..4a25026959 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -11,8 +11,51 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST from tests.async_mock import Mock, patch TEST_HOST = "1.2.3.4" +TEST_HOST2 = "5.6.7.8" TEST_API_KEY = "12ab345c-d67e-8f" -TEST_DEVICE_LIST = {"mac": Mock()} +TEST_MAC = "ab:cd:ef:gh" +TEST_MAC2 = "ij:kl:mn:op" +TEST_DEVICE_LIST = {TEST_MAC: Mock()} + +TEST_DISCOVERY_1 = { + TEST_HOST: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + {"mac": "abcdefghujkl0002", "deviceType": "10000000"}, + ], + } +} + +TEST_DISCOVERY_2 = { + TEST_HOST: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + ], + }, + TEST_HOST2: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC2, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + ], + }, +} @pytest.fixture(name="motion_blinds_connect", autouse=True) @@ -27,6 +70,9 @@ def motion_blinds_connect_fixture(): ), patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", TEST_DEVICE_LIST, + ), patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_1, ), patch( "homeassistant.components.motion_blinds.async_setup_entry", return_value=True ): @@ -45,7 +91,16 @@ async def test_config_flow_manual_host_success(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, ) assert result["type"] == "create_entry" @@ -56,6 +111,87 @@ async def test_config_flow_manual_host_success(hass): } +async def test_config_flow_discovery_1_success(hass): + """Successful flow with 1 gateway discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + } + + +async def test_config_flow_discovery_2_success(hass): + """Successful flow with 2 gateway discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_2, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "select" + assert result["data_schema"].schema["select_ip"].container == [ + TEST_HOST, + TEST_HOST2, + ] + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"select_ip": TEST_HOST2}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST2, + CONF_API_KEY: TEST_API_KEY, + } + + async def test_config_flow_connection_error(hass): """Failed flow manually initialized by the user with connection timeout.""" result = await hass.config_entries.flow.async_init( @@ -66,14 +202,47 @@ async def test_config_flow_connection_error(hass): assert result["step_id"] == "user" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + with patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", side_effect=socket.timeout, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + {CONF_API_KEY: TEST_API_KEY}, ) assert result["type"] == "abort" assert result["reason"] == "connection_error" + + +async def test_config_flow_discovery_fail(hass): + """Failed flow with no gateways discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value={}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "discovery_error"} From 035f9412ba3f1c66c64cdedef579ab82555004d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Dec 2020 12:16:39 -1000 Subject: [PATCH 252/302] Fix template triggers from time events (#44603) Co-authored-by: Paulus Schoutsen --- homeassistant/components/template/trigger.py | 47 ++++++------ tests/components/template/test_trigger.py | 77 +++++++++++++++++++- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 5d748edb84..80ad585486 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -52,25 +52,33 @@ async def async_attach_trigger( if not result_as_boolean(result): return - entity_id = event.data.get("entity_id") - from_s = event.data.get("old_state") - to_s = event.data.get("new_state") + entity_id = event and event.data.get("entity_id") + from_s = event and event.data.get("old_state") + to_s = event and event.data.get("new_state") + + if entity_id is not None: + description = f"{entity_id} via template" + else: + description = "time change or manual update via template" + + template_variables = { + "platform": platform_type, + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + } + trigger_variables = { + "for": time_delta, + "description": description, + } @callback def call_action(*_): """Call action with right context.""" + nonlocal trigger_variables hass.async_run_hass_job( job, - { - "trigger": { - "platform": "template", - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period, - "description": f"{entity_id} via template", - } - }, + {"trigger": {**template_variables, **trigger_variables}}, (to_s.context if to_s else None), ) @@ -78,18 +86,9 @@ async def async_attach_trigger( call_action() return - variables = { - "trigger": { - "platform": platform_type, - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - } - } - try: period = cv.positive_time_period( - template.render_complex(time_delta, variables) + template.render_complex(time_delta, {"trigger": template_variables}) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( @@ -97,6 +96,8 @@ async def async_attach_trigger( ) return + trigger_variables["for"] = period + delay_cancel = async_call_later(hass, period.seconds, call_action) info = async_track_template_result( diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 828cf1fb7b..822a274bf2 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -11,6 +11,7 @@ from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -626,6 +627,7 @@ async def test_if_fires_on_change_with_for_0_advanced(hass, calls): async def test_if_fires_on_change_with_for_2(hass, calls): """Test for firing on change with for.""" + context = Context() assert await async_setup_component( hass, automation.DOMAIN, @@ -636,17 +638,33 @@ async def test_if_fires_on_change_with_for_2(hass, calls): "value_template": "{{ is_state('test.entity', 'world') }}", "for": 5, }, - "action": {"service": "test.automation"}, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, } }, ) - hass.states.async_set("test.entity", "world") + hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + assert calls[0].data["some"] == "template - test.entity - hello - world - 0:00:05" async def test_if_not_fires_on_change_with_for(hass, calls): @@ -811,3 +829,58 @@ async def test_invalid_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() assert mock_logger.error.called + + +async def test_if_fires_on_time_change(hass, calls): + """Test for firing on time changes.""" + start_time = dt_util.utcnow() + timedelta(hours=24) + time_that_will_not_match_right_away = start_time.replace(minute=1, second=0) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "template", + "value_template": "{{ utcnow().minute % 2 == 0 }}", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Trigger once (match template) + first_time = start_time.replace(minute=2, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=first_time): + async_fire_time_changed(hass, first_time) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (match template) + second_time = start_time.replace(minute=4, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=second_time): + async_fire_time_changed(hass, second_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (do not match template) + third_time = start_time.replace(minute=5, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=third_time): + async_fire_time_changed(hass, third_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (match template) + forth_time = start_time.replace(minute=8, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=forth_time): + async_fire_time_changed(hass, forth_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 2 From 62237adf91e542ce389163917b62e0ced1cf3f50 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Dec 2020 00:19:38 +0100 Subject: [PATCH 253/302] Updated frontend to 20201229.0 (#44632) --- 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 caf309e671..abe98b98c8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201212.0"], + "requirements": ["home-assistant-frontend==20201229.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cdec1fd7b..7eb736c003 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201212.0 +home-assistant-frontend==20201229.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index ea6b464f66..c7d955b8b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201212.0 +home-assistant-frontend==20201229.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73503ac0e1..2e038049fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201212.0 +home-assistant-frontend==20201229.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a750c95d2e7e8a51f126b9602aaffa133efb5ce1 Mon Sep 17 00:00:00 2001 From: Matt Bilodeau Date: Tue, 29 Dec 2020 20:43:44 -0500 Subject: [PATCH 254/302] Add OutdoorPlug to wemo (#44629) --- homeassistant/components/wemo/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 32913c3722..75ca322b9a 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -30,6 +30,7 @@ WEMO_MODEL_DISPATCH = { "LightSwitch": SWITCH_DOMAIN, "Maker": SWITCH_DOMAIN, "Motion": BINARY_SENSOR_DOMAIN, + "OutdoorPlug": SWITCH_DOMAIN, "Sensor": BINARY_SENSOR_DOMAIN, "Socket": SWITCH_DOMAIN, } From 35a19a4d025379f1259af640ab881f9195c97c2e Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Tue, 29 Dec 2020 20:54:04 -0500 Subject: [PATCH 255/302] Bump env_canada to 0.2.5 (#44631) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 2a51c6ffd8..02a60049f0 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.4"], + "requirements": ["env_canada==0.2.5"], "codeowners": ["@michaeldavie"] } diff --git a/requirements_all.txt b/requirements_all.txt index c7d955b8b4..5209f0b29d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -550,7 +550,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.2.4 +env_canada==0.2.5 # homeassistant.components.envirophat # envirophat==0.0.6 From 12aa537eb95873786aea179a90ddce62d9bde0a6 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 29 Dec 2020 20:43:02 -0600 Subject: [PATCH 256/302] Support homekit discovery for roku (#44625) * support homekit discovery for roku * Update config_flow.py * Update config_flow.py * Update test_config_flow.py * Update __init__.py * Update __init__.py * Update strings.json * Update manifest.json * Update __init__.py * Update test_config_flow.py * Update __init__.py * Update manifest.json * Update config_flow.py * Update config_flow.py * Update __init__.py * Update test_config_flow.py * Update __init__.py * Update manifest.json * Update __init__.py * Update zeroconf.py * Update config_flow.py * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update __init__.py * Update config_flow.py * Update test_config_flow.py * Update manifest.json * Update zeroconf.py --- homeassistant/components/roku/config_flow.py | 45 +++++++++- homeassistant/components/roku/manifest.json | 8 ++ homeassistant/components/roku/strings.json | 2 +- homeassistant/generated/zeroconf.py | 4 + tests/components/roku/__init__.py | 16 +++- tests/components/roku/test_config_flow.py | 93 +++++++++++++++++++- 6 files changed, 160 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 6e494ce269..f8e9034292 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -85,6 +85,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) + async def async_step_homekit(self, discovery_info): + """Handle a flow initialized by homekit discovery.""" + + # If we already have the host configured do + # not open connections to it if we can avoid it. + if self._host_already_configured(discovery_info[CONF_HOST]): + return self.async_abort(reason="already_configured") + + self.discovery_info.update({CONF_HOST: discovery_info[CONF_HOST]}) + + try: + info = await validate_input(self.hass, self.discovery_info) + except RokuError: + _LOGGER.debug("Roku Error", exc_info=True) + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error trying to connect") + return self.async_abort(reason=ERROR_UNKNOWN) + + await self.async_set_unique_id(info["serial_number"]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: discovery_info[CONF_HOST]}, + ) + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update({"title_placeholders": {"name": info["title"]}}) + self.discovery_info.update({CONF_NAME: info["title"]}) + + return await self.async_step_discovery_confirm() + async def async_step_ssdp( self, discovery_info: Optional[Dict] = None ) -> Dict[str, Any]: @@ -110,16 +140,16 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unknown error trying to connect") return self.async_abort(reason=ERROR_UNKNOWN) - return await self.async_step_ssdp_confirm() + return await self.async_step_discovery_confirm() - async def async_step_ssdp_confirm( + async def async_step_discovery_confirm( self, user_input: Optional[Dict] = None ) -> Dict[str, Any]: """Handle user-confirmation of discovered device.""" # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 if user_input is None: return self.async_show_form( - step_id="ssdp_confirm", + step_id="discovery_confirm", description_placeholders={"name": self.discovery_info[CONF_NAME]}, errors={}, ) @@ -128,3 +158,12 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): title=self.discovery_info[CONF_NAME], data=self.discovery_info, ) + + def _host_already_configured(self, host): + """See if we already have a hub with the host address configured.""" + existing_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries() + if CONF_HOST in entry.data + } + return host in existing_hosts diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 39b48b91a8..682576b534 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,6 +3,14 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["rokuecp==0.6.0"], + "homekit": { + "models": [ + "3810X", + "4660X", + "7820X", + "C105X" + ] + }, "ssdp": [ { "st": "roku:ecp", diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 6d9000b866..55b533d4f1 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -8,7 +8,7 @@ "host": "[%key:common::config_flow::data::host%]" } }, - "ssdp_confirm": { + "discovery_confirm": { "title": "Roku", "description": "Do you want to set up {name}?", "data": {} diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6efa44e304..57b6e6cb12 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -157,10 +157,14 @@ ZEROCONF = { } HOMEKIT = { + "3810X": "roku", + "4660X": "roku", + "7820X": "roku", "819LMB": "myq", "AC02": "tado", "Abode": "abode", "BSB002": "hue", + "C105X": "roku", "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX": "lifx", diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index f2da007b5e..4ab2991bd4 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -8,14 +8,16 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -HOST = "192.168.1.160" NAME = "Roku 3" +NAME_ROKUTV = '58" Onn Roku TV' + +HOST = "192.168.1.160" SSDP_LOCATION = "http://192.168.1.160/" UPNP_FRIENDLY_NAME = "My Roku 3" UPNP_SERIAL = "1GU48T017973" @@ -26,6 +28,16 @@ MOCK_SSDP_DISCOVERY_INFO = { ATTR_UPNP_SERIAL: UPNP_SERIAL, } +HOMEKIT_HOST = "192.168.1.161" + +MOCK_HOMEKIT_DISCOVERY_INFO = { + CONF_NAME: "onn._hap._tcp.local.", + CONF_HOST: HOMEKIT_HOST, + "properties": { + CONF_ID: "2d:97:da:ee:dc:99", + }, +} + def mock_connection( aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index a3cda6afa6..16e4a434dc 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Roku config flow.""" from homeassistant.components.roku.const import DOMAIN -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -12,8 +12,11 @@ from homeassistant.setup import async_setup_component from tests.async_mock import patch from tests.components.roku import ( + HOMEKIT_HOST, HOST, + MOCK_HOMEKIT_DISCOVERY_INFO, MOCK_SSDP_DISCOVERY_INFO, + NAME_ROKUTV, UPNP_FRIENDLY_NAME, mock_connection, setup_integration, @@ -128,6 +131,92 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: assert len(mock_validate_input.mock_calls) == 1 +async def test_homekit_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort homekit flow on connection error.""" + mock_connection( + aioclient_mock, + host=HOMEKIT_HOST, + error=True, + ) + + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_HOMEKIT}, + data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_homekit_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort homekit flow on unknown error.""" + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + with patch( + "homeassistant.components.roku.config_flow.Roku.update", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_HOMEKIT}, + data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_homekit_discovery( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the homekit discovery flow.""" + mock_connection(aioclient_mock, device="rokutv", host=HOMEKIT_HOST) + + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} + + with patch( + "homeassistant.components.roku.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roku.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME_ROKUTV + + assert result["data"] + assert result["data"][CONF_HOST] == HOMEKIT_HOST + assert result["data"][CONF_NAME] == NAME_ROKUTV + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # test abort on existing host + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + async def test_ssdp_cannot_connect( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: @@ -176,7 +265,7 @@ async def test_ssdp_discovery( ) assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "ssdp_confirm" + assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} with patch( From 9cc768b34c316fba85796b4fffaa8a1c7cf9bd3f Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 30 Dec 2020 08:44:44 +0000 Subject: [PATCH 257/302] Bump pycarwings2 to 2.10 (#44634) --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 339b575003..db78e5ce0e 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,6 +2,6 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.9"], + "requirements": ["pycarwings2==2.10"], "codeowners": ["@filcole"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5209f0b29d..8e0dd465bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1292,7 +1292,7 @@ pyblackbird==0.5 pybotvac==0.0.19 # homeassistant.components.nissan_leaf -pycarwings2==2.9 +pycarwings2==2.10 # homeassistant.components.cloudflare pycfdns==1.2.1 From ee194b9411d46a785017649fabc4aacfba8def98 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Dec 2020 09:55:18 +0100 Subject: [PATCH 258/302] Initial Verisure cleanups (#44639) --- CODEOWNERS | 1 + homeassistant/components/verisure/__init__.py | 75 +++++++++---------- .../verisure/alarm_control_panel.py | 14 ++-- homeassistant/components/verisure/camera.py | 32 ++++---- homeassistant/components/verisure/const.py | 28 +++++++ homeassistant/components/verisure/lock.py | 14 ++-- .../components/verisure/manifest.json | 2 +- homeassistant/components/verisure/sensor.py | 3 +- 8 files changed, 92 insertions(+), 77 deletions(-) create mode 100644 homeassistant/components/verisure/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 66c5801e39..a660d93012 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -493,6 +493,7 @@ homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 homeassistant/components/vera/* @vangorra +homeassistant/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff @ludeeus homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 8cd8b0672c..2348d42a0d 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,7 +1,5 @@ """Support for Verisure devices.""" from datetime import timedelta -import logging -import threading from jsonpath import jsonpath import verisure @@ -18,30 +16,27 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) - -ATTR_DEVICE_SERIAL = "device_serial" - -CONF_ALARM = "alarm" -CONF_CODE_DIGITS = "code_digits" -CONF_DOOR_WINDOW = "door_window" -CONF_GIID = "giid" -CONF_HYDROMETERS = "hygrometers" -CONF_LOCKS = "locks" -CONF_DEFAULT_LOCK_CODE = "default_lock_code" -CONF_MOUSE = "mouse" -CONF_SMARTPLUGS = "smartplugs" -CONF_THERMOMETERS = "thermometers" -CONF_SMARTCAM = "smartcam" - -DOMAIN = "verisure" - -MIN_SCAN_INTERVAL = timedelta(minutes=1) -DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) - -SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" -SERVICE_DISABLE_AUTOLOCK = "disable_autolock" -SERVICE_ENABLE_AUTOLOCK = "enable_autolock" +from .const import ( + ATTR_DEVICE_SERIAL, + CONF_ALARM, + CONF_CODE_DIGITS, + CONF_DEFAULT_LOCK_CODE, + CONF_DOOR_WINDOW, + CONF_GIID, + CONF_HYDROMETERS, + CONF_LOCKS, + CONF_MOUSE, + CONF_SMARTCAM, + CONF_SMARTPLUGS, + CONF_THERMOMETERS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, + MIN_SCAN_INTERVAL, + SERVICE_CAPTURE_SMARTCAM, + SERVICE_DISABLE_AUTOLOCK, + SERVICE_ENABLE_AUTOLOCK, +) HUB = None @@ -101,9 +96,9 @@ def setup(hass, config): device_id = service.data[ATTR_DEVICE_SERIAL] try: await hass.async_add_executor_job(HUB.smartcam_capture, device_id) - _LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) + LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) except verisure.Error as ex: - _LOGGER.error("Could not capture image, %s", ex) + LOGGER.error("Could not capture image, %s", ex) hass.services.register( DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA @@ -114,9 +109,9 @@ def setup(hass, config): device_id = service.data[ATTR_DEVICE_SERIAL] try: await hass.async_add_executor_job(HUB.disable_autolock, device_id) - _LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) + LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) except verisure.Error as ex: - _LOGGER.error("Could not disable autolock, %s", ex) + LOGGER.error("Could not disable autolock, %s", ex) hass.services.register( DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA @@ -127,9 +122,9 @@ def setup(hass, config): device_id = service.data[ATTR_DEVICE_SERIAL] try: await hass.async_add_executor_job(HUB.enable_autolock, device_id) - _LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) + LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) except verisure.Error as ex: - _LOGGER.error("Could not enable autolock, %s", ex) + LOGGER.error("Could not enable autolock, %s", ex) hass.services.register( DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA @@ -147,8 +142,6 @@ class VerisureHub: self.config = domain_config - self._lock = threading.Lock() - self.session = verisure.Session( domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD] ) @@ -160,7 +153,7 @@ class VerisureHub: try: self.session.login() except verisure.Error as ex: - _LOGGER.error("Could not log in to verisure, %s", ex) + LOGGER.error("Could not log in to verisure, %s", ex) return False if self.giid: return self.set_giid() @@ -171,7 +164,7 @@ class VerisureHub: try: self.session.logout() except verisure.Error as ex: - _LOGGER.error("Could not log out from verisure, %s", ex) + LOGGER.error("Could not log out from verisure, %s", ex) return False return True @@ -180,7 +173,7 @@ class VerisureHub: try: self.session.set_giid(self.giid) except verisure.Error as ex: - _LOGGER.error("Could not set installation GIID, %s", ex) + LOGGER.error("Could not set installation GIID, %s", ex) return False return True @@ -189,9 +182,9 @@ class VerisureHub: try: self.overview = self.session.get_overview() except verisure.ResponseError as ex: - _LOGGER.error("Could not read overview, %s", ex) + LOGGER.error("Could not read overview, %s", ex) if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable - _LOGGER.info("Trying to log in again") + LOGGER.info("Trying to log in again") self.login() else: raise @@ -217,7 +210,7 @@ class VerisureHub: def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" res = jsonpath(self.overview, jpath % args) - return res if res else [] + return res or [] def get_first(self, jpath, *args): """Get first value from the overview that matches the jsonpath.""" @@ -227,4 +220,4 @@ class VerisureHub: def get_image_info(self, jpath, *args): """Get values from the imageseries that matches the jsonpath.""" res = jsonpath(self.imageseries, jpath % args) - return res if res else [] + return res or [] diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 239396b2d0..fff58433a9 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -1,5 +1,4 @@ """Support for Verisure alarm control panels.""" -import logging from time import sleep import homeassistant.components.alarm_control_panel as alarm @@ -13,9 +12,8 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from . import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, HUB as hub - -_LOGGER = logging.getLogger(__name__) +from . import HUB as hub +from .const import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, LOGGER def setup_platform(hass, config, add_entities, discovery_info=None): @@ -32,12 +30,12 @@ def set_arm_state(state, code=None): transaction_id = hub.session.set_arm_state(code, state)[ "armStateChangeTransactionId" ] - _LOGGER.info("verisure set arm state %s", state) + LOGGER.info("verisure set arm state %s", state) transaction = {} while "result" not in transaction: sleep(0.5) transaction = hub.session.get_arm_state_transaction(transaction_id) - hub.update_overview(no_throttle=True) + hub.update_overview() class VerisureAlarm(alarm.AlarmControlPanelEntity): @@ -58,7 +56,7 @@ class VerisureAlarm(alarm.AlarmControlPanelEntity): if giid in aliass: return "{} alarm".format(aliass[giid]) - _LOGGER.error("Verisure installation giid not found: %s", giid) + LOGGER.error("Verisure installation giid not found: %s", giid) return "{} alarm".format(hub.session.installations[0]["alias"]) @@ -93,7 +91,7 @@ class VerisureAlarm(alarm.AlarmControlPanelEntity): elif status == "ARMED_AWAY": self._state = STATE_ALARM_ARMED_AWAY elif status != "PENDING": - _LOGGER.error("Unknown alarm state %s", status) + LOGGER.error("Unknown alarm state %s", status) self._changed_by = hub.get_first("$.armState.name") def alarm_disarm(self, code=None): diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 73b27ee7d2..a69e1fb95d 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,14 +1,12 @@ """Support for Verisure cameras.""" import errno -import logging import os from homeassistant.components.camera import Camera from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from . import CONF_SMARTCAM, HUB as hub - -_LOGGER = logging.getLogger(__name__) +from . import HUB as hub +from .const import CONF_SMARTCAM, LOGGER def setup_platform(hass, config, add_entities, discovery_info=None): @@ -17,16 +15,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return False directory_path = hass.config.config_dir if not os.access(directory_path, os.R_OK): - _LOGGER.error("file path %s is not readable", directory_path) + LOGGER.error("file path %s is not readable", directory_path) return False hub.update_overview() - smartcams = [] - smartcams.extend( - [ - VerisureSmartcam(hass, device_label, directory_path) - for device_label in hub.get("$.customerImageCameras[*].deviceLabel") - ] - ) + smartcams = [ + VerisureSmartcam(hass, device_label, directory_path) + for device_label in hub.get("$.customerImageCameras[*].deviceLabel") + ] + add_entities(smartcams) @@ -47,9 +43,9 @@ class VerisureSmartcam(Camera): """Return image response.""" self.check_imagelist() if not self._image: - _LOGGER.debug("No image to display") + LOGGER.debug("No image to display") return - _LOGGER.debug("Trying to open %s", self._image) + LOGGER.debug("Trying to open %s", self._image) with open(self._image, "rb") as file: return file.read() @@ -63,14 +59,14 @@ class VerisureSmartcam(Camera): return new_image_id = image_ids[0] if new_image_id in ("-1", self._image_id): - _LOGGER.debug("The image is the same, or loading image_id") + LOGGER.debug("The image is the same, or loading image_id") return - _LOGGER.debug("Download new image %s", new_image_id) + LOGGER.debug("Download new image %s", new_image_id) new_image_path = os.path.join( self._directory_path, "{}{}".format(new_image_id, ".jpg") ) hub.session.download_image(self._device_label, new_image_id, new_image_path) - _LOGGER.debug("Old image_id=%s", self._image_id) + LOGGER.debug("Old image_id=%s", self._image_id) self.delete_image(self) self._image_id = new_image_id @@ -83,7 +79,7 @@ class VerisureSmartcam(Camera): ) try: os.remove(remove_image) - _LOGGER.debug("Deleting old image %s", remove_image) + LOGGER.debug("Deleting old image %s", remove_image) except OSError as error: if error.errno != errno.ENOENT: raise diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py new file mode 100644 index 0000000000..89dcfa396a --- /dev/null +++ b/homeassistant/components/verisure/const.py @@ -0,0 +1,28 @@ +"""Constants for the Verisure integration.""" +from datetime import timedelta +import logging + +DOMAIN = "verisure" + +LOGGER = logging.getLogger(__package__) + +ATTR_DEVICE_SERIAL = "device_serial" + +CONF_ALARM = "alarm" +CONF_CODE_DIGITS = "code_digits" +CONF_DOOR_WINDOW = "door_window" +CONF_GIID = "giid" +CONF_HYDROMETERS = "hygrometers" +CONF_LOCKS = "locks" +CONF_DEFAULT_LOCK_CODE = "default_lock_code" +CONF_MOUSE = "mouse" +CONF_SMARTPLUGS = "smartplugs" +CONF_THERMOMETERS = "thermometers" +CONF_SMARTCAM = "smartcam" + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) +MIN_SCAN_INTERVAL = timedelta(minutes=1) + +SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" +SERVICE_DISABLE_AUTOLOCK = "disable_autolock" +SERVICE_ENABLE_AUTOLOCK = "enable_autolock" diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 28efb64c71..228c8c6c17 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -1,13 +1,11 @@ """Support for Verisure locks.""" -import logging from time import monotonic, sleep from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED -from . import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, HUB as hub - -_LOGGER = logging.getLogger(__name__) +from . import HUB as hub +from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, LOGGER def setup_platform(hass, config, add_entities, discovery_info=None): @@ -83,7 +81,7 @@ class VerisureDoorlock(LockEntity): elif status == "LOCKED": self._state = STATE_LOCKED elif status != "PENDING": - _LOGGER.error("Unknown lock state %s", status) + LOGGER.error("Unknown lock state %s", status) self._changed_by = hub.get_first( "$.doorLockStatusList[?(@.deviceLabel=='%s')].userString", self._device_label, @@ -101,7 +99,7 @@ class VerisureDoorlock(LockEntity): code = kwargs.get(ATTR_CODE, self._default_lock_code) if code is None: - _LOGGER.error("Code required but none provided") + LOGGER.error("Code required but none provided") return self.set_lock_state(code, STATE_UNLOCKED) @@ -113,7 +111,7 @@ class VerisureDoorlock(LockEntity): code = kwargs.get(ATTR_CODE, self._default_lock_code) if code is None: - _LOGGER.error("Code required but none provided") + LOGGER.error("Code required but none provided") return self.set_lock_state(code, STATE_LOCKED) @@ -124,7 +122,7 @@ class VerisureDoorlock(LockEntity): transaction_id = hub.session.set_lock_state( code, self._device_label, lock_state )["doorLockStateChangeTransactionId"] - _LOGGER.debug("Verisure doorlock %s", state) + LOGGER.debug("Verisure doorlock %s", state) transaction = {} attempts = 0 while "result" not in transaction: diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 13c2936497..6260f4a9ff 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -3,5 +3,5 @@ "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": ["jsonpath==0.82", "vsure==1.5.4"], - "codeowners": [] + "codeowners": ["@frenck"] } diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 437e45ba72..ac7c8f40e8 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -2,7 +2,8 @@ from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS, HUB as hub +from . import HUB as hub +from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS def setup_platform(hass, config, add_entities, discovery_info=None): From eb07282e9bc744899807b0074de9dbcf29bb28d9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 30 Dec 2020 01:03:27 -0800 Subject: [PATCH 259/302] Add debug logging for failed OAuth token refreshes to help users diagnose (#44637) --- .../helpers/config_entry_oauth2_flow.py | 7 +++++ .../helpers/test_config_entry_oauth2_flow.py | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 526a774cc3..c4d7de3839 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -191,6 +191,13 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): data["client_secret"] = self.client_secret resp = await session.post(self.token_url, data=data) + if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): + body = await resp.text() + _LOGGER.debug( + "Token request failed with status=%s, body=%s", + resp.status, + body, + ) resp.raise_for_status() return cast(dict, await resp.json()) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 157bbf3bc2..f2f2db37d7 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -3,6 +3,7 @@ import asyncio import logging import time +import aiohttp import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -546,3 +547,32 @@ async def test_implementation_provider(hass, local_impl): assert await config_entry_oauth2_flow.async_get_implementations( hass, mock_domain_with_impl ) == {TEST_DOMAIN: local_impl, "cloud": provider_source[mock_domain_with_impl]} + + +async def test_oauth_session_refresh_failure( + hass, flow_handler, local_impl, aioclient_mock +): + """Test the OAuth2 session helper when no refresh is needed.""" + flow_handler.async_register_implementation(hass, local_impl) + + aioclient_mock.post(TOKEN_URL, status=400) + + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "auth_implementation": TEST_DOMAIN, + "token": { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + # Already expired, requires a refresh + "expires_in": -500, + "expires_at": time.time() - 500, + "token_type": "bearer", + "random_other_data": "should_stay", + }, + }, + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) + with pytest.raises(aiohttp.client_exceptions.ClientResponseError): + await session.async_request("post", "https://example.com") From a212248f8d1b2f9961f925a9268edf55632fcc1f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 30 Dec 2020 10:22:09 +0100 Subject: [PATCH 260/302] Upgrade psutil to 5.8.0 (#44640) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 3b08a7afe1..9ea39b6388 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.7.2"], + "requirements": ["psutil==5.8.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 8e0dd465bf..565b6b4c34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ prometheus_client==0.7.1 proxmoxer==1.1.1 # homeassistant.components.systemmonitor -psutil==5.7.2 +psutil==5.8.0 # homeassistant.components.ptvsd ptvsd==4.3.2 From baacf2cd7decc9e689a436a6bf1e993e19812146 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 30 Dec 2020 01:23:48 -0800 Subject: [PATCH 261/302] Publish timestamps in nest events (#44641) --- homeassistant/components/nest/__init__.py | 1 + tests/components/nest/test_device_trigger.py | 21 ++++++++---- tests/components/nest/test_events.py | 34 +++++++++++++++----- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index e4be96cbf1..1240d30f02 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -138,6 +138,7 @@ class SignalUpdateCallback: message = { "device_id": device_entry.id, "type": event_type, + "timestamp": event_message.timestamp, } self._hass.bus.async_fire(NEST_EVENT, message) diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 29091f32f5..b7c7586215 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -10,6 +10,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.events import NEST_EVENT from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform @@ -213,7 +214,7 @@ async def test_fires_on_camera_motion(hass, calls): """Test camera_motion triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_motion") - message = {"device_id": DEVICE_ID, "type": "camera_motion"} + message = {"device_id": DEVICE_ID, "type": "camera_motion", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -224,7 +225,7 @@ async def test_fires_on_camera_person(hass, calls): """Test camera_person triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_person") - message = {"device_id": DEVICE_ID, "type": "camera_person"} + message = {"device_id": DEVICE_ID, "type": "camera_person", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -235,7 +236,7 @@ async def test_fires_on_camera_sound(hass, calls): """Test camera_person triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_sound") - message = {"device_id": DEVICE_ID, "type": "camera_sound"} + message = {"device_id": DEVICE_ID, "type": "camera_sound", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -246,7 +247,7 @@ async def test_fires_on_doorbell_chime(hass, calls): """Test doorbell_chime triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "doorbell_chime") - message = {"device_id": DEVICE_ID, "type": "doorbell_chime"} + message = {"device_id": DEVICE_ID, "type": "doorbell_chime", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -257,7 +258,11 @@ async def test_trigger_for_wrong_device_id(hass, calls): """Test for turn_on and turn_off triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_motion") - message = {"device_id": "wrong-device-id", "type": "camera_motion"} + message = { + "device_id": "wrong-device-id", + "type": "camera_motion", + "timestamp": utcnow(), + } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 0 @@ -267,7 +272,11 @@ async def test_trigger_for_wrong_event_type(hass, calls): """Test for turn_on and turn_off triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_motion") - message = {"device_id": DEVICE_ID, "type": "wrong-event-type"} + message = { + "device_id": DEVICE_ID, + "type": "wrong-event-type", + "timestamp": utcnow(), + } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 0 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 12314f6056..7295d13408 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -54,7 +54,7 @@ def create_device_traits(event_trait): } -def create_event(event_type, device_id=DEVICE_ID): +def create_event(event_type, device_id=DEVICE_ID, timestamp=None): """Create an EventMessage for a single event type.""" events = { event_type: { @@ -65,12 +65,14 @@ def create_event(event_type, device_id=DEVICE_ID): return create_events(events=events, device_id=device_id) -def create_events(events, device_id=DEVICE_ID): +def create_events(events, device_id=DEVICE_ID, timestamp=None): """Create an EventMessage for events.""" + if not timestamp: + timestamp = utcnow() return EventMessage( { "eventId": "some-event-id", - "timestamp": utcnow().isoformat(timespec="seconds"), + "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": device_id, "events": events, @@ -102,15 +104,18 @@ async def test_doorbell_chime_event(hass): assert device.model == "Doorbell" assert device.identifiers == {("nest", DEVICE_ID)} + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.DoorbellChime.Chime") + create_event("sdm.devices.events.DoorbellChime.Chime", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "doorbell_chime", + "timestamp": event_time, } @@ -126,15 +131,18 @@ async def test_camera_motion_event(hass): entry = registry.async_get("camera.front") assert entry is not None + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraMotion.Motion") + create_event("sdm.devices.events.CameraMotion.Motion", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "camera_motion", + "timestamp": event_time, } @@ -150,15 +158,18 @@ async def test_camera_sound_event(hass): entry = registry.async_get("camera.front") assert entry is not None + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraSound.Sound") + create_event("sdm.devices.events.CameraSound.Sound", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "camera_sound", + "timestamp": event_time, } @@ -174,15 +185,18 @@ async def test_camera_person_event(hass): entry = registry.async_get("camera.front") assert entry is not None + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraPerson.Person") + create_event("sdm.devices.events.CameraPerson.Person", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "camera_person", + "timestamp": event_time, } @@ -209,17 +223,21 @@ async def test_camera_multiple_event(hass): }, } - await subscriber.async_receive_event(create_events(event_map)) + timestamp = utcnow() + await subscriber.async_receive_event(create_events(event_map, timestamp=timestamp)) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 2 assert events[0].data == { "device_id": entry.device_id, "type": "camera_motion", + "timestamp": event_time, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", + "timestamp": event_time, } From 338938a38e88c3e7862f70b88763a99adde0024c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 30 Dec 2020 10:29:54 +0100 Subject: [PATCH 262/302] Fix shelly shutdown AttributeError (#44172) * Additional check for clean shutdown * Changed approach * Remover leftover * Added callback key * Moved to listen once --- homeassistant/components/shelly/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 298c7e111b..cd08b625a5 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -143,6 +143,8 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): ) self._last_input_events_count = dict() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + @callback def _async_input_events_handler(self): """Handle device input events.""" @@ -184,6 +186,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self): """Fetch data.""" + _LOGGER.debug("Polling Shelly Device - %s", self.name) try: async with async_timeout.timeout( @@ -206,6 +209,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def async_setup(self): """Set up the wrapper.""" + dev_reg = await device_registry.async_get_registry(self.hass) model_type = self.device.settings["device"]["type"] entry = dev_reg.async_get_or_create( @@ -225,6 +229,12 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device.shutdown() self._async_remove_input_events_handler() + @callback + def _handle_ha_stop(self, _): + """Handle Home Assistant stopping.""" + _LOGGER.debug("Stopping ShellyDeviceWrapper for %s", self.name) + self.shutdown() + class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" From 6e5e45b9372f4c22fdac54a97bed2828d1c85874 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 30 Dec 2020 11:24:00 +0100 Subject: [PATCH 263/302] Upgrade youtube_dl to 2020.12.29 (#44643) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 3c07fbc4e2..2c17d85f7b 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.12.07"], + "requirements": ["youtube_dl==2020.12.29"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index 565b6b4c34..8477f615aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2327,7 +2327,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.12.07 +youtube_dl==2020.12.29 # homeassistant.components.onvif zeep[async]==4.0.0 From 4b5056a45623f36a8ebac9d5954c5be185cee91b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Dec 2020 12:18:22 +0100 Subject: [PATCH 264/302] Bumped version to 2021.1.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de18fd4ed0..2ff32fc839 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 335aceedfbc7768d5671aeef10e24a02b9fc2e71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Dec 2020 12:10:42 -1000 Subject: [PATCH 265/302] Update py-august to 0.25.2 to fix august token refreshes (#40109) * Update py-august to 0.26.0 to fix august token refreshes * bump version --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index c2c383468f..67649b7edb 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.25.0"], + "requirements": ["py-august==0.25.2"], "dependencies": ["configurator"], "codeowners": ["@bdraco"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 8477f615aa..cdf9eedec8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1192,7 +1192,7 @@ pushover_complete==1.1.1 pwmled==1.6.7 # homeassistant.components.august -py-august==0.25.0 +py-august==0.25.2 # homeassistant.components.canary py-canary==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e038049fa..22246aa4ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -597,7 +597,7 @@ pure-python-adb[async]==0.3.0.dev0 pushbullet.py==0.11.0 # homeassistant.components.august -py-august==0.25.0 +py-august==0.25.2 # homeassistant.components.canary py-canary==0.5.0 From e65903822de83fd28c925abacc7525901e4b90ad Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 1 Jan 2021 06:35:05 -0600 Subject: [PATCH 266/302] Suppress vizio logging API call failures to prevent no-op logs (#44388) --- .../components/vizio/media_player.py | 43 +++++++++++-------- tests/components/vizio/test_config_flow.py | 1 + tests/components/vizio/test_media_player.py | 27 ++++++++++-- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 61c9ca5485..4c06c89692 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -184,10 +184,10 @@ class VizioDevice(MediaPlayerEntity): async def async_update(self) -> None: """Retrieve latest state of the device.""" if not self._model: - self._model = await self._device.get_model_name() + self._model = await self._device.get_model_name(log_api_exception=False) if not self._sw_version: - self._sw_version = await self._device.get_version() + self._sw_version = await self._device.get_version(log_api_exception=False) is_on = await self._device.get_power_state(log_api_exception=False) @@ -236,7 +236,9 @@ class VizioDevice(MediaPlayerEntity): if not self._available_sound_modes: self._available_sound_modes = ( await self._device.get_setting_options( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + log_api_exception=False, ) ) else: @@ -306,6 +308,7 @@ class VizioDevice(MediaPlayerEntity): setting_type, setting_name, new_value, + log_api_exception=False, ) async def async_added_to_hass(self) -> None: @@ -453,52 +456,58 @@ class VizioDevice(MediaPlayerEntity): """Select sound mode.""" if sound_mode in self._available_sound_modes: await self._device.set_setting( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE, sound_mode + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + sound_mode, + log_api_exception=False, ) async def async_turn_on(self) -> None: """Turn the device on.""" - await self._device.pow_on() + await self._device.pow_on(log_api_exception=False) async def async_turn_off(self) -> None: """Turn the device off.""" - await self._device.pow_off() + await self._device.pow_off(log_api_exception=False) async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: - await self._device.mute_on() + await self._device.mute_on(log_api_exception=False) self._is_volume_muted = True else: - await self._device.mute_off() + await self._device.mute_off(log_api_exception=False) self._is_volume_muted = False async def async_media_previous_track(self) -> None: """Send previous channel command.""" - await self._device.ch_down() + await self._device.ch_down(log_api_exception=False) async def async_media_next_track(self) -> None: """Send next channel command.""" - await self._device.ch_up() + await self._device.ch_up(log_api_exception=False) async def async_select_source(self, source: str) -> None: """Select input source.""" if source in self._available_inputs: - await self._device.set_input(source) + await self._device.set_input(source, log_api_exception=False) elif source in self._get_additional_app_names(): await self._device.launch_app_config( **next( app["config"] for app in self._additional_app_configs if app["name"] == source - ) + ), + log_api_exception=False, ) elif source in self._available_apps: - await self._device.launch_app(source, self._all_apps) + await self._device.launch_app( + source, self._all_apps, log_api_exception=False + ) async def async_volume_up(self) -> None: """Increase volume of the device.""" - await self._device.vol_up(num=self._volume_step) + await self._device.vol_up(num=self._volume_step, log_api_exception=False) if self._volume_level is not None: self._volume_level = min( @@ -507,7 +516,7 @@ class VizioDevice(MediaPlayerEntity): async def async_volume_down(self) -> None: """Decrease volume of the device.""" - await self._device.vol_down(num=self._volume_step) + await self._device.vol_down(num=self._volume_step, log_api_exception=False) if self._volume_level is not None: self._volume_level = max( @@ -519,10 +528,10 @@ class VizioDevice(MediaPlayerEntity): if self._volume_level is not None: if volume > self._volume_level: num = int(self._max_volume * (volume - self._volume_level)) - await self._device.vol_up(num=num) + await self._device.vol_up(num=num, log_api_exception=False) self._volume_level = volume elif volume < self._volume_level: num = int(self._max_volume * (self._volume_level - volume)) - await self._device.vol_down(num=num) + await self._device.vol_down(num=num, log_api_exception=False) self._volume_level = volume diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index e966188afd..5f33aa2be4 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -858,6 +858,7 @@ async def test_zeroconf_ignore( async def test_zeroconf_no_unique_id( hass: HomeAssistantType, + vizio_guess_device_type: pytest.fixture, vizio_no_unique_id: pytest.fixture, ) -> None: """Test zeroconf discovery aborts when unique_id is None.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 996d46e08a..0d11ec2289 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -40,6 +40,7 @@ from homeassistant.components.vizio.const import ( CONF_ADDITIONAL_CONFIGS, CONF_APPS, CONF_VOLUME_STEP, + DEFAULT_VOLUME_STEP, DOMAIN, SERVICE_UPDATE_SETTING, VIZIO_SCHEMA, @@ -259,6 +260,7 @@ async def _test_service( **kwargs, ) -> None: """Test generic Vizio media player entity service.""" + kwargs["log_api_exception"] = False service_data = {ATTR_ENTITY_ID: ENTITY_ID} if additional_service_data: service_data.update(additional_service_data) @@ -378,13 +380,27 @@ async def test_services( {ATTR_INPUT_SOURCE: "USB"}, "USB", ) - await _test_service(hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None) - await _test_service(hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None) await _test_service( - hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=DEFAULT_VOLUME_STEP ) await _test_service( - hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None, num=DEFAULT_VOLUME_STEP + ) + await _test_service( + hass, + MP_DOMAIN, + "vol_up", + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 1}, + num=(100 - 15), + ) + await _test_service( + hass, + MP_DOMAIN, + "vol_down", + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 0}, + num=(15 - 0), ) await _test_service(hass, MP_DOMAIN, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) await _test_service(hass, MP_DOMAIN, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) @@ -394,6 +410,9 @@ async def test_services( "set_setting", SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"}, + "audio", + "eq", + "Music", ) # Test that the update_setting service does config validation/transformation correctly await _test_service( From 8a689a010548d86e049b165234d580d5a976c555 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 31 Dec 2020 00:02:56 +0100 Subject: [PATCH 267/302] Add motion binary sensor (#44445) --- homeassistant/components/shelly/binary_sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 53038352d4..d53f089054 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, @@ -70,6 +71,9 @@ SENSORS = { default_enabled=False, removal_condition=is_momentary_input, ), + ("sensor", "motion"): BlockAttributeDescription( + name="Motion", device_class=DEVICE_CLASS_MOTION + ), } REST_SENSORS = { From 1b26f6e8e0f81ae86290949232dc45217b0818cf Mon Sep 17 00:00:00 2001 From: Sian Date: Sat, 2 Jan 2021 01:36:36 +1030 Subject: [PATCH 268/302] Correct Dyson climate fan auto mode (#44569) Co-authored-by: Justin Gauthier --- homeassistant/components/dyson/climate.py | 8 ++++++-- tests/components/dyson/test_climate.py | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index d2c23f4609..a71c124c63 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -2,6 +2,7 @@ import logging from libpurecool.const import ( + AutoMode, FanPower, FanSpeed, FanState, @@ -333,7 +334,10 @@ class DysonPureHotCoolEntity(ClimateEntity): @property def fan_mode(self): """Return the fan setting.""" - if self._device.state.fan_state == FanState.FAN_OFF.value: + if ( + self._device.state.auto_mode != AutoMode.AUTO_ON.value + and self._device.state.fan_state == FanState.FAN_OFF.value + ): return FAN_OFF return SPEED_MAP[self._device.state.speed] @@ -368,7 +372,7 @@ class DysonPureHotCoolEntity(ClimateEntity): elif fan_mode == FAN_HIGH: self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) elif fan_mode == FAN_AUTO: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_AUTO) + self._device.enable_auto_mode() def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 77105dc73d..c4e4c91087 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -677,8 +677,7 @@ async def test_purehotcool_set_fan_mode(devices, login, hass): {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_AUTO}, True, ) - assert device.set_fan_speed.call_count == 4 - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_AUTO) + assert device.enable_auto_mode.call_count == 1 @patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) From 470fe887a529195e2d62d9e2069f781b4112e875 Mon Sep 17 00:00:00 2001 From: Mark Allanson Date: Thu, 31 Dec 2020 00:16:53 +0000 Subject: [PATCH 269/302] Upgrade canary integration to use py-canary 0.5.1 (#44645) Fixes #35569 --- homeassistant/components/canary/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index b4598d6408..af6b0ce54b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,7 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": ["py-canary==0.5.0"], + "requirements": ["py-canary==0.5.1"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index cdf9eedec8..4369011a58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1195,7 +1195,7 @@ pwmled==1.6.7 py-august==0.25.2 # homeassistant.components.canary -py-canary==0.5.0 +py-canary==0.5.1 # homeassistant.components.cpuspeed py-cpuinfo==7.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22246aa4ec..0153cfee3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -600,7 +600,7 @@ pushbullet.py==0.11.0 py-august==0.25.2 # homeassistant.components.canary -py-canary==0.5.0 +py-canary==0.5.1 # homeassistant.components.melissa py-melissa-climate==2.1.4 From 7d99a35547e6cc27e30f176f362ed4583335ba44 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 30 Dec 2020 09:11:08 -0500 Subject: [PATCH 270/302] Bump ZHA quirks version to 0.0.50 (#44650) --- 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 ed2460614f..ebca96da5f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.21.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.49", + "zha-quirks==0.0.50", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", "zigpy==0.28.2", diff --git a/requirements_all.txt b/requirements_all.txt index 4369011a58..e74131e347 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.49 +zha-quirks==0.0.50 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0153cfee3d..7d4bf31e2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.49 +zha-quirks==0.0.50 # homeassistant.components.zha zigpy-cc==0.5.2 From 3cd97398aa8a3bf6d49c38d2c295c030bced5993 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Wed, 30 Dec 2020 18:15:27 +0000 Subject: [PATCH 271/302] Bump zm-py version to 0.5.2 (#44658) --- homeassistant/components/zoneminder/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index b3a87510e5..039513f100 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -2,6 +2,6 @@ "domain": "zoneminder", "name": "ZoneMinder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", - "requirements": ["zm-py==0.4.0"], + "requirements": ["zm-py==0.5.2"], "codeowners": ["@rohankapoorcom"] } diff --git a/requirements_all.txt b/requirements_all.txt index e74131e347..26b139a905 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,4 +2366,4 @@ zigpy-znp==0.3.0 zigpy==0.28.2 # homeassistant.components.zoneminder -zm-py==0.4.0 +zm-py==0.5.2 From 610ee24bb15f0a23d914d594983e50dba234334b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 30 Dec 2020 23:39:14 +0000 Subject: [PATCH 272/302] always sync unit_of_measurement (#44670) --- homeassistant/components/utility_meter/sensor.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 9a4ed9e778..6b25ec7d12 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -145,13 +145,7 @@ class UtilityMeterSensor(RestoreEntity): ): return - if ( - self._unit_of_measurement is None - and new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is not None - ): - self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT - ) + self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) try: diff = Decimal(new_state.state) - Decimal(old_state.state) From 2b1df2e63facc22c18ec2c6dbb1ebd44f5871c39 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jan 2021 20:54:43 +0100 Subject: [PATCH 273/302] Catch Shelly zeroconf types with uppercase too (#44672) Co-authored-by: Franck Nijhof --- homeassistant/components/shelly/config_flow.py | 3 --- tests/components/shelly/test_config_flow.py | 11 ----------- 2 files changed, 14 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 261c1898ca..b3dd7bb80f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -138,9 +138,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, zeroconf_info): """Handle zeroconf discovery.""" - if not zeroconf_info.get("name", "").startswith("shelly"): - return self.async_abort(reason="not_shelly") - try: self.info = info = await self._async_get_info(zeroconf_info["host"]) except HTTP_CONNECT_ERRORS: diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 71c971757c..2850be1145 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -488,14 +488,3 @@ async def test_zeroconf_require_auth(hass): } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_zeroconf_not_shelly(hass): - """Test we filter out non-shelly devices.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={"host": "1.1.1.1", "name": "notshelly"}, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - assert result["type"] == "abort" - assert result["reason"] == "not_shelly" From 34a6b4deb08321152fe5db8330aa9a9b44f67a12 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 31 Dec 2020 10:22:24 -0800 Subject: [PATCH 274/302] Fix legacy nest api binary_sensor initialization (#44674) --- .../components/nest/binary_sensor.py | 2 +- .../components/nest/legacy/binary_sensor.py | 2 +- tests/components/nest/test_init_legacy.py | 87 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/components/nest/test_init_legacy.py diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index dc58dd2856..d49ec8535c 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SDM -from .legacy.sensor import async_setup_legacy_entry +from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py index 4470bd1467..32c30f747d 100644 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities): """Set up a Nest binary sensor based on a config entry.""" nest = hass.data[DATA_NEST] diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py new file mode 100644 index 0000000000..f85fcdaa74 --- /dev/null +++ b/tests/components/nest/test_init_legacy.py @@ -0,0 +1,87 @@ +"""Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" + +import time + +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + +DOMAIN = "nest" + +CONFIG = { + "nest": { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + }, +} + +CONFIG_ENTRY_DATA = { + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, +} + + +def make_thermostat(): + """Make a mock thermostat with dummy values.""" + device = MagicMock() + type(device).device_id = PropertyMock(return_value="a.b.c.d.e.f.g") + type(device).name = PropertyMock(return_value="My Thermostat") + type(device).name_long = PropertyMock(return_value="My Thermostat") + type(device).serial = PropertyMock(return_value="serial-number") + type(device).mode = "off" + type(device).hvac_state = "off" + type(device).target = PropertyMock(return_value=31.0) + type(device).temperature = PropertyMock(return_value=30.1) + type(device).min_temperature = PropertyMock(return_value=10.0) + type(device).max_temperature = PropertyMock(return_value=50.0) + type(device).humidity = PropertyMock(return_value=40.4) + type(device).software_version = PropertyMock(return_value="a.b.c") + return device + + +async def test_thermostat(hass): + """Test simple initialization for thermostat entities.""" + + thermostat = make_thermostat() + + structure = MagicMock() + type(structure).name = PropertyMock(return_value="My Room") + type(structure).thermostats = PropertyMock(return_value=[thermostat]) + type(structure).eta = PropertyMock(return_value="away") + + nest = MagicMock() + type(nest).structures = PropertyMock(return_value=[structure]) + + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( + "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", + ["humidity", "temperature"], + ), patch( + "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", + {"fan": None}, + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() + + climate = hass.states.get("climate.my_thermostat") + assert climate is not None + assert climate.state == "off" + + temperature = hass.states.get("sensor.my_thermostat_temperature") + assert temperature is not None + assert temperature.state == "-1.1" + + humidity = hass.states.get("sensor.my_thermostat_humidity") + assert humidity is not None + assert humidity.state == "40.4" + + fan = hass.states.get("binary_sensor.my_thermostat_fan") + assert fan is not None + assert fan.state == "on" From 1c2e4226dc34f023a7a7ef759972a2ecba1d73e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Dec 2020 01:06:26 +0100 Subject: [PATCH 275/302] Zeroconf lowercase (#44675) --- .../components/brother/manifest.json | 2 +- homeassistant/components/zeroconf/__init__.py | 36 +++++++++++-------- homeassistant/generated/zeroconf.py | 2 +- script/hassfest/manifest.py | 20 +++++++++-- tests/components/zeroconf/test_init.py | 8 +++-- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 0e534147cb..9bb9ba0026 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], "requirements": ["brother==0.1.20"], - "zeroconf": [{"type": "_printer._tcp.local.", "name":"Brother*"}], + "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 68300adbcf..fdf4b98faf 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -284,22 +284,30 @@ async def _async_start_zeroconf_browser(hass, zeroconf): # likely bad homekit data return + if "name" in info: + lowercase_name = info["name"].lower() + else: + lowercase_name = None + + if "macaddress" in info.get("properties", {}): + uppercase_mac = info["properties"]["macaddress"].upper() + else: + uppercase_mac = None + for entry in zeroconf_types[service_type]: if len(entry) > 1: - if "macaddress" in entry: - if "properties" not in info: - continue - if "macaddress" not in info["properties"]: - continue - if not fnmatch.fnmatch( - info["properties"]["macaddress"], entry["macaddress"] - ): - continue - if "name" in entry: - if "name" not in info: - continue - if not fnmatch.fnmatch(info["name"], entry["name"]): - continue + if ( + uppercase_mac is not None + and "macaddress" in entry + and not fnmatch.fnmatch(uppercase_mac, entry["macaddress"]) + ): + continue + if ( + lowercase_name is not None + and "name" in entry + and not fnmatch.fnmatch(lowercase_name, entry["name"]) + ): + continue hass.add_job( hass.config_entries.flow.async_init( diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 57b6e6cb12..49527666f5 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -116,7 +116,7 @@ ZEROCONF = { "_printer._tcp.local.": [ { "domain": "brother", - "name": "Brother*" + "name": "brother*" } ], "_spotify-connect._tcp.local.": [ diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 389e380af8..7500483ec5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -33,6 +33,22 @@ def documentation_url(value: str) -> str: return value +def verify_lowercase(value: str): + """Verify a value is lowercase.""" + if value.lower() != value: + raise vol.Invalid("Value needs to be lowercase") + + return value + + +def verify_uppercase(value: str): + """Verify a value is uppercase.""" + if value.upper() != value: + raise vol.Invalid("Value needs to be uppercase") + + return value + + MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, @@ -45,8 +61,8 @@ MANIFEST_SCHEMA = vol.Schema( vol.Schema( { vol.Required("type"): str, - vol.Optional("macaddress"): str, - vol.Optional("name"): str, + vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("name"): vol.All(str, verify_lowercase), } ), ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 8767953b36..6b79c55291 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -242,13 +242,17 @@ async def test_zeroconf_match(hass, mock_zeroconf): handlers[0]( zeroconf, "_http._tcp.local.", - "shelly108._http._tcp.local.", + "Shelly108._http._tcp.local.", ServiceStateChange.Added, ) with patch.dict( zc_gen.ZEROCONF, - {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + { + "_http._tcp.local.": [ + {"domain": "shelly", "name": "shelly*", "macaddress": "FFAADD*"} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" From 864546201ebce130dd727dabd358dd1a5c2132cd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 31 Dec 2020 08:07:15 -0500 Subject: [PATCH 276/302] Bump up ZHA dependencies (#44680) - zigpy == 0.29.0 - zigpy_deconz == 0.11.1 - zha-quirks == 0.0.51 --- 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 ebca96da5f..54fceda03a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,10 +7,10 @@ "bellows==0.21.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.50", + "zha-quirks==0.0.51", "zigpy-cc==0.5.2", - "zigpy-deconz==0.11.0", - "zigpy==0.28.2", + "zigpy-deconz==0.11.1", + "zigpy==0.29.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.3.0" diff --git a/requirements_all.txt b/requirements_all.txt index 26b139a905..b238a79713 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.50 +zha-quirks==0.0.51 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2351,7 +2351,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.11.0 +zigpy-deconz==0.11.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2363,7 +2363,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.2 +zigpy==0.29.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d4bf31e2e..4c9dc5cc37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,13 +1144,13 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.50 +zha-quirks==0.0.51 # homeassistant.components.zha zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.11.0 +zigpy-deconz==0.11.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1162,4 +1162,4 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.2 +zigpy==0.29.0 From 39b9821d2956f0f41234e086479c50f669a22bcb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 31 Dec 2020 14:04:12 -0800 Subject: [PATCH 277/302] Fix broken test test_auto_purge in recorder (#44687) * Fix failing test due to edge-of-2021 bug * Rewrite test_auto_purge to make the intent more clear --- tests/components/recorder/test_init.py | 45 +++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index fcda7b0bb6..41c1f52b99 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -23,7 +23,7 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done from tests.async_mock import patch -from tests.common import async_fire_time_changed, get_test_home_assistant +from tests.common import fire_time_changed, get_test_home_assistant def test_saving_state(hass, hass_recorder): @@ -351,8 +351,15 @@ async def test_defaults_set(hass): assert recorder_config["purge_keep_days"] == 10 +def run_tasks_at_time(hass, test_time): + """Advance the clock and wait for any callbacks to finish.""" + fire_time_changed(hass, test_time) + hass.block_till_done() + hass.data[DATA_INSTANCE].block_till_done() + + def test_auto_purge(hass_recorder): - """Test saving and restoring a state.""" + """Test periodic purge alarm scheduling.""" hass = hass_recorder() original_tz = dt_util.DEFAULT_TIME_ZONE @@ -360,18 +367,40 @@ def test_auto_purge(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) + # Purging is schedule to happen at 4:12am every day. Exercise this behavior + # by firing alarms and advancing the clock around this time. Pick an arbitrary + # year in the future to avoid boundary conditions relative to the current date. + # + # The clock is started at 4:15am then advanced forward below now = dt_util.utcnow() - test_time = tz.localize(datetime(now.year + 1, 1, 1, 4, 12, 0)) - async_fire_time_changed(hass, test_time) + test_time = tz.localize(datetime(now.year + 2, 1, 1, 4, 15, 0)) + run_tasks_at_time(hass, test_time) with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data: - for delta in (-1, 0, 1): - async_fire_time_changed(hass, test_time + timedelta(seconds=delta)) - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() + # Advance one day, and the purge task should run + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 1 + purge_old_data.reset_mock() + + # Advance one day, and the purge task should run again + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 1 + + purge_old_data.reset_mock() + + # Advance less than one full day. The alarm should not yet fire. + test_time = test_time + timedelta(hours=23) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 0 + + # Advance to the next day and fire the alarm again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 dt_util.set_default_time_zone(original_tz) From 6d33f6a115b50bf809778b634f518291a23b42e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 02:03:34 -1000 Subject: [PATCH 278/302] Fix script wait templates with now/utcnow (#44717) --- homeassistant/helpers/event.py | 6 +++--- homeassistant/helpers/script.py | 15 +++++++++++---- tests/helpers/test_event.py | 27 +++++++++++++++++++++++++++ tests/helpers/test_script.py | 26 ++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 661e1a11b5..f06ac8aca3 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -715,9 +715,9 @@ def async_track_template( hass.async_run_hass_job( job, - event.data.get("entity_id"), - event.data.get("old_state"), - event.data.get("new_state"), + event and event.data.get("entity_id"), + event and event.data.get("old_state"), + event and event.data.get("new_state"), ) info = async_track_template_result( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 48a662e3a8..77c842a27f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -62,7 +62,11 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import condition, config_validation as cv, service, template -from homeassistant.helpers.event import async_call_later, async_track_template +from homeassistant.helpers.event import ( + TrackTemplate, + async_call_later, + async_track_template_result, +) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.trigger import ( async_initialize_triggers, @@ -355,7 +359,7 @@ class _ScriptRun: return @callback - def async_script_wait(entity_id, from_s, to_s): + def _async_script_wait(event, updates): """Handle script after template condition is true.""" self._variables["wait"] = { "remaining": to_context.remaining if to_context else delay, @@ -364,9 +368,12 @@ class _ScriptRun: done.set() to_context = None - unsub = async_track_template( - self._hass, wait_template, async_script_wait, self._variables + info = async_track_template_result( + self._hass, + [TrackTemplate(wait_template, self._variables)], + _async_script_wait, ) + unsub = info.async_remove self._changed() done = asyncio.Event() diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 1b9ddc5219..9c7ddb09f8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -908,6 +908,33 @@ async def test_track_template_error_can_recover(hass, caplog): assert "UndefinedError" not in caplog.text +async def test_track_template_time_change(hass, caplog): + """Test tracking template with time change.""" + template_error = Template("{{ utcnow().minute % 2 == 0 }}", hass) + calls = [] + + @ha.callback + def error_callback(entity_id, old_state, new_state): + calls.append((entity_id, old_state, new_state)) + + start_time = dt_util.utcnow() + timedelta(hours=24) + time_that_will_not_match_right_away = start_time.replace(minute=1, second=0) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + async_track_template(hass, template_error, error_callback) + await hass.async_block_till_done() + assert not calls + + first_time = start_time.replace(minute=2, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=first_time): + async_fire_time_changed(hass, first_time) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0] == (None, None, None) + + async def test_track_template_result(hass): """Test tracking template.""" specific_runs = [] diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 92666335f2..c81ed681d4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -780,6 +780,32 @@ async def test_wait_template_variables_in(hass): assert not script_obj.is_running +async def test_wait_template_with_utcnow(hass): + """Test the wait template with utcnow.""" + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hours == 12 }}"}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + start_time = dt_util.utcnow() + timedelta(hours=24) + + try: + hass.async_create_task(script_obj.async_run(context=Context())) + async_fire_time_changed(hass, start_time.replace(hour=5)) + assert not script_obj.is_running + async_fire_time_changed(hass, start_time.replace(hour=12)) + + await asyncio.wait_for(wait_started_flag.wait(), 1) + + assert script_obj.is_running + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, start_time.replace(hour=3)) + await hass.async_block_till_done() + + assert not script_obj.is_running + + @pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_variables_out(hass, mode, action_type): From af1d46aa6c42f260b050dd4469326ad9a2e4a26d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 01:40:08 -1000 Subject: [PATCH 279/302] Fix rest notify GET without params configured (#44723) --- homeassistant/components/rest/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 3e4f97d5bc..15b871cde5 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -197,7 +197,7 @@ class RestNotificationService(BaseNotificationService): response = requests.get( self._resource, headers=self._headers, - params=self._params.update(data), + params={**self._params, **data} if self._params else data, timeout=10, auth=self._auth, verify=self._verify_ssl, From c60390a52aea39491ffffff1a46ad255ded58dd2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 08:28:20 -1000 Subject: [PATCH 280/302] Fix templates for rest notify (#44724) --- homeassistant/components/rest/notify.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 15b871cde5..f15df42864 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -30,6 +30,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from . import DOMAIN, PLATFORMS @@ -56,8 +57,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string, vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string, - vol.Optional(CONF_DATA): dict, - vol.Optional(CONF_DATA_TEMPLATE): {cv.match_all: cv.template_complex}, + vol.Optional(CONF_DATA): vol.All(dict, cv.template_complex), + vol.Optional(CONF_DATA_TEMPLATE): vol.All(dict, cv.template_complex), vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -155,9 +156,7 @@ class RestNotificationService(BaseNotificationService): # integrations, so just return the first target in the list. data[self._target_param_name] = kwargs[ATTR_TARGET][0] - if self._data: - data.update(self._data) - elif self._data_template: + if self._data_template or self._data: kwargs[ATTR_MESSAGE] = message def _data_template_creator(value): @@ -168,10 +167,15 @@ class RestNotificationService(BaseNotificationService): return { key: _data_template_creator(item) for key, item in value.items() } + if not isinstance(value, Template): + return value value.hass = self._hass return value.async_render(kwargs, parse_result=False) - data.update(_data_template_creator(self._data_template)) + if self._data: + data.update(_data_template_creator(self._data)) + if self._data_template: + data.update(_data_template_creator(self._data_template)) if self._method == "POST": response = requests.post( From e781e1b26c95bf21a4f95dc84a606fc6b4752f7b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Jan 2021 18:39:59 +0100 Subject: [PATCH 281/302] Bump H11 library to support non RFC line endings (#44735) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7eb736c003..6f25618871 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -37,6 +37,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f627346c67..130fd2cc24 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -65,6 +65,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 From 79aad3f07bb485599fce2ea5db2d11a8c1567830 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 21:14:18 +0100 Subject: [PATCH 282/302] Bumped version to 2021.1.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2ff32fc839..8a6d0f5893 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 92e354ca38d051639aa73bae15b820b6b6d6bef2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2021 01:34:10 +0100 Subject: [PATCH 283/302] Guard unbound var for DSMR (#44673) --- homeassistant/components/dsmr/sensor.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 0c53fbd607..78cd317bb3 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -197,6 +197,10 @@ async def async_setup_entry( async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" + stop_listener = None + transport = None + protocol = None + while hass.state != CoreState.stopping: # Start DSMR asyncio.Protocol reader try: @@ -211,10 +215,9 @@ async def async_setup_entry( # Wait for reader to close await protocol.wait_closed() - # Unexpected disconnect - if transport: - # remove listener - stop_listener() + # Unexpected disconnect + if not hass.is_stopping: + stop_listener() transport = None protocol = None @@ -234,7 +237,7 @@ async def async_setup_entry( protocol = None except CancelledError: if stop_listener: - stop_listener() + stop_listener() # pylint: disable=not-callable if transport: transport.close() From 8da79479d3ef6dce44686dba507686407c5e3110 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Jan 2021 13:14:07 +0100 Subject: [PATCH 284/302] Change rest sensors update interval for Shelly Motion (#44692) * Change rest sensors update interval for Shelly Motion * Cleaning * Fix typo * Remove unnecessary parentheses --- homeassistant/components/shelly/__init__.py | 12 +++++++++++- homeassistant/components/shelly/const.py | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cd08b625a5..147d9fb950 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers import ( ) from .const import ( + BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, DOMAIN, @@ -241,12 +242,21 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): def __init__(self, hass, device: aioshelly.Device): """Initialize the Shelly device wrapper.""" + if ( + device.settings["device"]["type"] + in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION + ): + update_interval = ( + SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + else: + update_interval = REST_SENSORS_UPDATE_INTERVAL super().__init__( hass, _LOGGER, name=get_device_name(device), - update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL), + update_interval=timedelta(seconds=update_interval), ) self.device = device diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cd74746697..9f5c5b2efc 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -32,3 +32,6 @@ INPUTS_EVENTS_DICT = { "SL": "single_long", "LS": "long_single", } + +# List of battery devices that maintain a permanent WiFi connection +BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] From f33c1332b9d8fa7c4b5c4017bc797ec0e02f03a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jan 2021 23:51:44 -1000 Subject: [PATCH 285/302] Add index to old_state_id column for postgres and older databases (#44757) * Add index to old_state_id column for older databases The schema was updated in #43610 but the index was not added on migration. * Handle postgresql missing ondelete * create index first --- homeassistant/components/recorder/migration.py | 12 +++++++++++- homeassistant/components/recorder/models.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index c633c114b4..4501b25385 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -211,7 +211,13 @@ def _update_states_table_with_foreign_key_options(engine): inspector = reflection.Inspector.from_engine(engine) alters = [] for foreign_key in inspector.get_foreign_keys(TABLE_STATES): - if foreign_key["name"] and not foreign_key["options"]: + if foreign_key["name"] and ( + # MySQL/MariaDB will have empty options + not foreign_key["options"] + or + # Postgres will have ondelete set to None + foreign_key["options"].get("ondelete") is None + ): alters.append( { "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]), @@ -312,6 +318,10 @@ def _apply_update(engine, new_version, old_version): _create_index(engine, "events", "ix_events_event_type_time_fired") _drop_index(engine, "events", "ix_events_event_type") elif new_version == 10: + # Now done in step 11 + pass + elif new_version == 11: + _create_index(engine, "states", "ix_states_old_state_id") _update_states_table_with_foreign_key_options(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 5b37f7e3f9..9481e954bd 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 10 +SCHEMA_VERSION = 11 _LOGGER = logging.getLogger(__name__) From 506fdc877a1dfc6b4e307b0bbb0d6c4a655cb910 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 2 Jan 2021 12:07:52 +0100 Subject: [PATCH 286/302] Update docker base image 2021.01.0 (#44761) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 49cee1ff28..a7ce097ae8 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2020.11.2", - "armhf": "homeassistant/armhf-homeassistant-base:2020.11.2", - "armv7": "homeassistant/armv7-homeassistant-base:2020.11.2", - "amd64": "homeassistant/amd64-homeassistant-base:2020.11.2", - "i386": "homeassistant/i386-homeassistant-base:2020.11.2" + "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.0", + "armhf": "homeassistant/armhf-homeassistant-base:2021.01.0", + "armv7": "homeassistant/armv7-homeassistant-base:2021.01.0", + "amd64": "homeassistant/amd64-homeassistant-base:2021.01.0", + "i386": "homeassistant/i386-homeassistant-base:2021.01.0" }, "labels": { "io.hass.type": "core" From f42ce2b0d1ae6a077a335c071082c2b55b6c2c4a Mon Sep 17 00:00:00 2001 From: Bob Matcuk Date: Mon, 4 Jan 2021 06:33:34 -0500 Subject: [PATCH 287/302] Fix bug with blink auth flow (#44769) --- homeassistant/components/blink/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index d244c31648..5c77add311 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -36,6 +36,7 @@ def _send_blink_2fa_pin(auth, pin): """Send 2FA pin to blink servers.""" blink = Blink() blink.auth = auth + blink.setup_login_ids() blink.setup_urls() return auth.send_auth_key(blink, pin) From 5f91f14a49995e936d732b5b803ff3ada99d4a05 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 4 Jan 2021 04:49:29 +0100 Subject: [PATCH 288/302] Fix knx.send service not accepting floats (#44802) --- homeassistant/components/knx/__init__.py | 25 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1d547e895b..3123031595 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -131,14 +131,23 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_KNX_SEND_SCHEMA = vol.Schema( - { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, - vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( - cv.positive_int, [cv.positive_int] - ), - vol.Optional(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), - } +SERVICE_KNX_SEND_SCHEMA = vol.Any( + vol.Schema( + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, + vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), + } + ), + vol.Schema( + # without type given payload is treated as raw bytes + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( + cv.positive_int, [cv.positive_int] + ), + } + ), ) From e4fcc9c692d737bcdaf5aa9fbb8828d12247d343 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 4 Jan 2021 13:17:44 +0100 Subject: [PATCH 289/302] Bumped version to 2021.1.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8a6d0f5893..dff0d120c9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From b3fda469cf0b28b873373c40841aef1ba345f7dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jan 2021 10:18:54 -1000 Subject: [PATCH 290/302] Fix zeroconf outgoing dns compression corruption for large packets (#44828) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 753ac2a244..654eec820c 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.7"], + "requirements": ["zeroconf==0.28.8"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6f25618871..1c9ddc70f5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.4.2 -zeroconf==0.28.7 +zeroconf==0.28.8 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index b238a79713..005c6e03e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2336,7 +2336,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.7 +zeroconf==0.28.8 # homeassistant.components.zha zha-quirks==0.0.51 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c9dc5cc37..96b7618c1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1141,7 +1141,7 @@ yeelight==0.5.4 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.28.7 +zeroconf==0.28.8 # homeassistant.components.zha zha-quirks==0.0.51 From 9396d9db5fad7226ccd76ca0e1e7086c87118713 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 5 Jan 2021 03:57:05 +0100 Subject: [PATCH 291/302] Implement color mode for ZHA light polling (#44829) --- homeassistant/components/zha/light.py | 35 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 4a25fa3c98..32b8a06405 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,6 +1,7 @@ """Lights on Zigbee Home Automation networks.""" from collections import Counter from datetime import timedelta +import enum import functools import itertools import logging @@ -88,6 +89,14 @@ SUPPORT_GROUP_LIGHT = ( ) +class LightColorMode(enum.IntEnum): + """ZCL light color mode enum.""" + + HS_COLOR = 0x00 + XY_COLOR = 0x01 + COLOR_TEMP = 0x02 + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" entities_to_create = hass.data[DATA_ZHA][light.DOMAIN] @@ -442,6 +451,7 @@ class Light(BaseLight, ZhaEntity): self._brightness = level if self._color_channel: attributes = [ + "color_mode", "color_temperature", "current_x", "current_y", @@ -452,16 +462,21 @@ class Light(BaseLight, ZhaEntity): attributes, from_cache=False ) - color_temp = results.get("color_temperature") - if color_temp is not None: - self._color_temp = color_temp - - color_x = results.get("current_x") - color_y = results.get("current_y") - if color_x is not None and color_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(color_x / 65535), float(color_y / 65535) - ) + color_mode = results.get("color_mode") + if color_mode is not None: + if color_mode == LightColorMode.COLOR_TEMP: + color_temp = results.get("color_temperature") + if color_temp is not None and color_mode: + self._color_temp = color_temp + self._hs_color = None + else: + color_x = results.get("current_x") + color_y = results.get("current_y") + if color_x is not None and color_y is not None: + self._hs_color = color_util.color_xy_to_hs( + float(color_x / 65535), float(color_y / 65535) + ) + self._color_temp = None color_loop_active = results.get("color_loop_active") if color_loop_active is not None: From e4a84bb1c361fa662a89ad5b1893df1b6c87e5ce Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 5 Jan 2021 10:01:34 +0100 Subject: [PATCH 292/302] Bump pypck to 0.7.8 (#44834) --- homeassistant/components/lcn/__init__.py | 3 ++- homeassistant/components/lcn/binary_sensor.py | 17 ++++++++++------- homeassistant/components/lcn/climate.py | 5 +++-- homeassistant/components/lcn/cover.py | 3 ++- homeassistant/components/lcn/light.py | 6 ++++-- homeassistant/components/lcn/manifest.json | 8 ++++++-- homeassistant/components/lcn/sensor.py | 6 ++++-- homeassistant/components/lcn/switch.py | 6 ++++-- requirements_all.txt | 2 +- 9 files changed, 36 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 72f11b7b00..cc1e47d71f 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -139,7 +139,8 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - self.device_connection.register_for_inputs(self.input_received) + if not self.device_connection.is_group: + self.device_connection.register_for_inputs(self.input_received) @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 5d712045c9..415668f592 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -50,9 +50,10 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.setpoint_variable + ) @property def is_on(self): @@ -85,9 +86,10 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.bin_sensor_port + ) @property def is_on(self): @@ -116,7 +118,8 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index e3eb92a426..ece3994f65 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -63,8 +63,9 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index c5e407573b..3d7c2a06a3 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -161,7 +161,8 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.motor) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index c6ef895b7d..5242ed1cc5 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -68,7 +68,8 @@ class LcnOutputLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -155,7 +156,8 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index f07c4d9c64..919051d7e7 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,6 +2,10 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.7"], - "codeowners": ["@alengwenus"] + "requirements": [ + "pypck==0.7.8" + ], + "codeowners": [ + "@alengwenus" + ] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 26b54def97..4d4be5e125 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -57,7 +57,8 @@ class LcnVariableSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -98,7 +99,8 @@ class LcnLedLogicSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 5891629627..6f9cc25db9 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -50,7 +50,8 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -97,7 +98,8 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 005c6e03e9..76641ab9f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1604,7 +1604,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.7 +pypck==0.7.8 # homeassistant.components.pjlink pypjlink2==1.2.1 From 0b8251d9a1d14130b5adfab96c3c870e76c67cb6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jan 2021 17:35:28 +0100 Subject: [PATCH 293/302] Make Alexa custom ID unique (#44839) * Make Alexa custom ID unique * Lint * Lint --- homeassistant/components/alexa/config.py | 5 +++++ homeassistant/components/alexa/entities.py | 2 +- .../components/alexa/smart_home_http.py | 5 +++++ .../components/cloud/alexa_config.py | 19 ++++++++++++++--- homeassistant/components/cloud/client.py | 16 ++++++++------ .../components/cloud/google_config.py | 2 +- homeassistant/components/cloud/http_api.py | 9 +++++--- tests/components/alexa/__init__.py | 7 ++++++- tests/components/alexa/test_entities.py | 21 +++++++++++++++++++ tests/components/cloud/test_alexa_config.py | 17 ++++++++++----- 10 files changed, 83 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 7d3a3994ac..cc5c604dc8 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -45,6 +45,11 @@ class AbstractConfig(ABC): """Return if proactive mode is enabled.""" return self._unsub_proactive_report is not None + @callback + @abstractmethod + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + async def async_enable_proactive_mode(self): """Enable proactive mode.""" if self._unsub_proactive_report is None: diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 574ba6b8ba..c05d9641b9 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -329,7 +329,7 @@ class AlexaEntity: "manufacturer": "Home Assistant", "model": self.entity.domain, "softwareVersion": __version__, - "customIdentifier": self.entity_id, + "customIdentifier": f"{self.config.user_identifier()}-{self.entity_id}", }, } diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 41ebfb340e..41738c824f 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -53,6 +53,11 @@ class AlexaConfig(AbstractConfig): """Return config locale.""" return self._config.get(CONF_LOCALE) + @core.callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "" + def should_expose(self, entity_id): """If an entity should be exposed.""" return self._config[CONF_FILTER](entity_id) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 1bb74053ea..7abbefe85f 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -5,7 +5,7 @@ import logging import aiohttp import async_timeout -from hass_nabucasa import cloud_api +from hass_nabucasa import Cloud, cloud_api from homeassistant.components.alexa import ( config as alexa_config, @@ -14,7 +14,7 @@ from homeassistant.components.alexa import ( state_report as alexa_state_report, ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST -from homeassistant.core import callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow @@ -32,10 +32,18 @@ SYNC_DELAY = 1 class AlexaConfig(alexa_config.AbstractConfig): """Alexa Configuration.""" - def __init__(self, hass, config, prefs: CloudPreferences, cloud): + def __init__( + self, + hass: HomeAssistant, + config: dict, + cloud_user: str, + prefs: CloudPreferences, + cloud: Cloud, + ): """Initialize the Alexa config.""" super().__init__(hass) self._config = config + self._cloud_user = cloud_user self._prefs = prefs self._cloud = cloud self._token = None @@ -85,6 +93,11 @@ class AlexaConfig(alexa_config.AbstractConfig): """Return entity config.""" return self._config.get(CONF_ENTITY_CONFIG) or {} + @callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return self._cloud_user + def should_expose(self, entity_id): """If an entity should be exposed.""" if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 2a2d383f36..155a39e49b 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -79,13 +79,15 @@ class CloudClient(Interface): """Return true if we want start a remote connection.""" return self._prefs.remote_enabled - @property - def alexa_config(self) -> alexa_config.AlexaConfig: + async def get_alexa_config(self) -> alexa_config.AlexaConfig: """Return Alexa config.""" if self._alexa_config is None: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._alexa_config = alexa_config.AlexaConfig( - self._hass, self.alexa_user_config, self._prefs, self.cloud + self._hass, self.alexa_user_config, cloud_user, self._prefs, self.cloud ) return self._alexa_config @@ -110,8 +112,9 @@ class CloudClient(Interface): async def enable_alexa(_): """Enable Alexa.""" + aconf = await self.get_alexa_config() try: - await self.alexa_config.async_enable_proactive_mode() + await aconf.async_enable_proactive_mode() except aiohttp.ClientError as err: # If no internet available yet if self._hass.is_running: logging.getLogger(__package__).warning( @@ -133,7 +136,7 @@ class CloudClient(Interface): tasks = [] - if self.alexa_config.enabled and self.alexa_config.should_report_state: + if self._prefs.alexa_enabled and self._prefs.alexa_report_state: tasks.append(enable_alexa) if self._prefs.google_enabled: @@ -164,9 +167,10 @@ class CloudClient(Interface): async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" cloud_user = await self._prefs.get_cloud_user() + aconfig = await self.get_alexa_config() return await alexa_sh.async_handle_message( self._hass, - self.alexa_config, + aconfig, payload, context=Context(user_id=cloud_user), enabled=self._prefs.alexa_enabled, diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 4b5891359b..2ac0bc4025 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, cloud_user, prefs: CloudPreferences, cloud): + def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 3075f6a3f9..a4d8b84b1a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -397,9 +397,10 @@ async def websocket_update_prefs(hass, connection, msg): # If we turn alexa linking on, validate that we can fetch access token if changes.get(PREF_ALEXA_REPORT_STATE): + alexa_config = await cloud.client.get_alexa_config() try: with async_timeout.timeout(10): - await cloud.client.alexa_config.async_get_access_token() + await alexa_config.async_get_access_token() except asyncio.TimeoutError: connection.send_error( msg["id"], "alexa_timeout", "Timeout validating Alexa access token." @@ -555,7 +556,8 @@ async def google_assistant_update(hass, connection, msg): async def alexa_list(hass, connection, msg): """List all alexa entities.""" cloud = hass.data[DOMAIN] - entities = alexa_entities.async_get_entities(hass, cloud.client.alexa_config) + alexa_config = await cloud.client.get_alexa_config() + entities = alexa_entities.async_get_entities(hass, alexa_config) result = [] @@ -603,10 +605,11 @@ async def alexa_update(hass, connection, msg): async def alexa_sync(hass, connection, msg): """Sync with Alexa.""" cloud = hass.data[DOMAIN] + alexa_config = await cloud.client.get_alexa_config() with async_timeout.timeout(10): try: - success = await cloud.client.alexa_config.async_sync_entities() + success = await alexa_config.async_sync_entities() except alexa_errors.NoTokenAvailable: connection.send_error( msg["id"], diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index d9c1a5a40c..bc007fefb8 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -2,7 +2,7 @@ from uuid import uuid4 from homeassistant.components.alexa import config, smart_home -from homeassistant.core import Context +from homeassistant.core import Context, callback from tests.common import async_mock_service @@ -37,6 +37,11 @@ class MockConfig(config.AbstractConfig): """Return config locale.""" return TEST_LOCALE + @callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "mock-user-id" + def should_expose(self, entity_id): """If an entity should be exposed.""" return True diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 6b48c313fc..45991375ba 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,5 +1,6 @@ """Test Alexa entity representation.""" from homeassistant.components.alexa import smart_home +from homeassistant.const import __version__ from . import DEFAULT_CONFIG, get_new_request @@ -20,6 +21,26 @@ async def test_unsupported_domain(hass): assert not msg["payload"]["endpoints"] +async def test_serialize_discovery(hass): + """Test we handle an interface raising unexpectedly during serialize discovery.""" + request = get_new_request("Alexa.Discovery", "Discover") + + hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request) + + assert "event" in msg + msg = msg["event"] + endpoint = msg["payload"]["endpoints"][0] + + assert endpoint["additionalAttributes"] == { + "manufacturer": "Home Assistant", + "model": "switch", + "softwareVersion": __version__, + "customIdentifier": "mock-user-id-switch.bla", + } + + async def test_serialize_discovery_recovers(hass, caplog): """Test we handle an interface raising unexpectedly during serialize discovery.""" request = get_new_request("Alexa.Discovery", "Discover") diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index e54a5dcde0..ce76195292 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -16,7 +16,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): alexa_entity_configs={"light.kitchen": entity_conf}, alexa_default_expose=["light"], ) - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + ) assert not conf.should_expose("light.kitchen") entity_conf["should_expose"] = True @@ -33,7 +35,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): async def test_alexa_config_report_state(hass, cloud_prefs): """Test Alexa config should expose using prefs.""" - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + ) assert cloud_prefs.alexa_report_state is False assert conf.should_report_state is False @@ -68,6 +72,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): conf = alexa_config.AlexaConfig( hass, ALEXA_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", @@ -114,7 +119,7 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( @@ -147,7 +152,9 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Alexa config responds to entity registry.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data["cloud"]) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + ) with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( @@ -197,7 +204,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): async def test_alexa_update_report_state(hass, cloud_prefs): """Test Alexa config responds to reporting state.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) with patch( "homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities", From fdce5878c657e5ef7546f9ec9290da0a8bac55e8 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 5 Jan 2021 16:22:25 +0000 Subject: [PATCH 294/302] =?UTF-8?q?Bump=20openwebifpy=20version:=203.1.6?= =?UTF-8?q?=20=E2=86=92=203.2.7=20(#44847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index fe461654ec..da6765368a 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -2,6 +2,6 @@ "domain": "enigma2", "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", - "requirements": ["openwebifpy==3.1.6"], + "requirements": ["openwebifpy==3.2.7"], "codeowners": ["@fbradyirl"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76641ab9f7..2486c7317b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1058,7 +1058,7 @@ openhomedevice==0.7.2 opensensemap-api==0.1.5 # homeassistant.components.enigma2 -openwebifpy==3.1.6 +openwebifpy==3.2.7 # homeassistant.components.luci openwrt-luci-rpc==1.1.6 From 2ee50a4d547c6e603ca457754a443c34b45e22fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jan 2021 20:55:17 +0100 Subject: [PATCH 295/302] Fix Canary doing I/O in event loop (#44854) --- homeassistant/components/canary/camera.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index fd2f08c148..0493a964cc 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -127,11 +127,14 @@ class CanaryCamera(CoordinatorEntity, Camera): async def async_camera_image(self): """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) + live_stream_url = await self.hass.async_add_executor_job( + getattr, self._live_stream_session, "live_stream_url" + ) ffmpeg = ImageFrame(self._ffmpeg.binary) image = await asyncio.shield( ffmpeg.get_image( - self._live_stream_session.live_stream_url, + live_stream_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments, ) From 587676f436ea7960c5efd479d4a9291679e0836e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Jan 2021 23:27:35 +0100 Subject: [PATCH 296/302] Update frontend to 20201229.1 (#44861) --- 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 abe98b98c8..241b07fd59 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201229.0"], + "requirements": ["home-assistant-frontend==20201229.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1c9ddc70f5..11e7dd8991 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201229.0 +home-assistant-frontend==20201229.1 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2486c7317b..0cf2451432 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201229.0 +home-assistant-frontend==20201229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96b7618c1d..70249117ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201229.0 +home-assistant-frontend==20201229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c258c2653f7c6b1b7cfff0044b1ef5d694f10863 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jan 2021 23:31:18 +0100 Subject: [PATCH 297/302] Bumped version to 2021.1.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dff0d120c9..52254ad68e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 88eac0be85da203eefffd1e8e3c1102796a75c84 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 31 Dec 2020 01:18:58 +0100 Subject: [PATCH 298/302] Bump pytradfri to 7.0.6 (#44661) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 57f58f0599..5c6bf76a16 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.5"], + "requirements": ["pytradfri[async]==7.0.6"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 0cf2451432..4d1bd88a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1852,7 +1852,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.5 +pytradfri[async]==7.0.6 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70249117ee..278822f3c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -912,7 +912,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.5 +pytradfri[async]==7.0.6 # homeassistant.components.vera pyvera==0.3.11 From b6d323b0082a39540319e72fd4f135e5fb5d4b37 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 6 Jan 2021 07:12:19 -0600 Subject: [PATCH 299/302] Fix Plex media summary attribute (#44863) --- homeassistant/components/plex/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4d765cc050..24e37216b7 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -518,7 +518,7 @@ class PlexMediaPlayer(MediaPlayerEntity): "media_content_rating", "media_library_title", "player_source", - "summary", + "media_summary", "username", ]: value = getattr(self, attr, None) From 9d03b56c5c05ce28487d1e98a3a69dc1f410d82c Mon Sep 17 00:00:00 2001 From: treylok Date: Wed, 6 Jan 2021 06:40:24 -0600 Subject: [PATCH 300/302] Bump python-ecobee-api to 0.2.8 (#44866) --- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 38d6b4577b..040744b27a 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,6 +3,6 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": ["python-ecobee-api==0.2.7"], + "requirements": ["python-ecobee-api==0.2.8"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4d1bd88a62..cc6cd18574 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.7 +python-ecobee-api==0.2.8 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 278822f3c0..466c072f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -870,7 +870,7 @@ pysqueezebox==0.5.5 pysyncthru==0.7.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.7 +python-ecobee-api==0.2.8 # homeassistant.components.darksky python-forecastio==1.4.0 From 9c478e8de70a6be7fb5f76320bcc9200ee737e1c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jan 2021 16:03:04 +0100 Subject: [PATCH 301/302] Bumped version to 2021.1.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 52254ad68e..4baa7e7bc3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 57d119a7fe4296a1bb06e380d76fd8f97ddef443 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jan 2021 16:47:02 +0100 Subject: [PATCH 302/302] Revert "Bump pypck to 0.7.8" (#44884) This reverts commit addafd517f3617071468b2f4ae3fa31f655a9ed2. --- homeassistant/components/lcn/__init__.py | 3 +-- homeassistant/components/lcn/binary_sensor.py | 17 +++++++---------- homeassistant/components/lcn/climate.py | 5 ++--- homeassistant/components/lcn/cover.py | 3 +-- homeassistant/components/lcn/light.py | 6 ++---- homeassistant/components/lcn/manifest.json | 8 ++------ homeassistant/components/lcn/sensor.py | 6 ++---- homeassistant/components/lcn/switch.py | 6 ++---- requirements_all.txt | 2 +- 9 files changed, 20 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index cc1e47d71f..72f11b7b00 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -139,8 +139,7 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - if not self.device_connection.is_group: - self.device_connection.register_for_inputs(self.input_received) + self.device_connection.register_for_inputs(self.input_received) @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 415668f592..5d712045c9 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -50,10 +50,9 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) + await self.device_connection.activate_status_request_handler( + self.setpoint_variable + ) @property def is_on(self): @@ -86,10 +85,9 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) + await self.device_connection.activate_status_request_handler( + self.bin_sensor_port + ) @property def is_on(self): @@ -118,8 +116,7 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index ece3994f65..e3eb92a426 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -63,9 +63,8 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 3d7c2a06a3..c5e407573b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -161,8 +161,7 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.motor) + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 5242ed1cc5..c6ef895b7d 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -68,8 +68,7 @@ class LcnOutputLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -156,8 +155,7 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 919051d7e7..f07c4d9c64 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,10 +2,6 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": [ - "pypck==0.7.8" - ], - "codeowners": [ - "@alengwenus" - ] + "requirements": ["pypck==0.7.7"], + "codeowners": ["@alengwenus"] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 4d4be5e125..26b54def97 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -57,8 +57,7 @@ class LcnVariableSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -99,8 +98,7 @@ class LcnLedLogicSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 6f9cc25db9..5891629627 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -50,8 +50,7 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -98,8 +97,7 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index cc6cd18574..8e3b891c2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1604,7 +1604,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.8 +pypck==0.7.7 # homeassistant.components.pjlink pypjlink2==1.2.1