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 b57d8c97617..db8eb6b2456 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 fe3af4c1ee6..c6deb8e9f8f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -242,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py new file mode 100644 index 00000000000..ff984e2c0d3 --- /dev/null +++ b/homeassistant/components/kulersky/__init__.py @@ -0,0 +1,44 @@ +"""Kuler Sky lights integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Kuler Sky component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Kuler Sky from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py new file mode 100644 index 00000000000..2b22fcdbd31 --- /dev/null +++ b/homeassistant/components/kulersky/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Kuler Sky.""" +import logging + +import pykulersky + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + # Check if there are any devices that can be discovered in the network. + try: + devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + except pykulersky.PykulerskyException as exc: + _LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc) + return False + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py new file mode 100644 index 00000000000..ae1e7a435dc --- /dev/null +++ b/homeassistant/components/kulersky/const.py @@ -0,0 +1,2 @@ +"""Constants for the Kuler Sky integration.""" +DOMAIN = "kulersky" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py new file mode 100644 index 00000000000..4c17d1bcba3 --- /dev/null +++ b/homeassistant/components/kulersky/light.py @@ -0,0 +1,210 @@ +"""Kuler Sky light platform.""" +import asyncio +from datetime import timedelta +import logging +from typing import Callable, List + +import pykulersky + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE + +DISCOVERY_INTERVAL = timedelta(seconds=60) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Kuler sky light devices.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if "devices" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["devices"] = set() + if "discovery" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["discovery"] = asyncio.Lock() + + async def discover(*args): + """Attempt to discover new lights.""" + # Since discovery needs to connect to all discovered bluetooth devices, and + # only rules out devices after a timeout, it can potentially take a long + # time. If there's already a discovery running, just skip this poll. + if hass.data[DOMAIN]["discovery"].locked(): + return + + async with hass.data[DOMAIN]["discovery"]: + bluetooth_devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + + # Filter out already connected lights + new_devices = [ + device + for device in bluetooth_devices + if device["address"] not in hass.data[DOMAIN]["devices"] + ] + + for device in new_devices: + light = pykulersky.Light(device["address"], device["name"]) + try: + # Attempt to connect to this light and read the color. If the + # connection fails, either this is not a Kuler Sky light, or + # it's bluetooth connection is currently locked by another + # device. If the vendor's app is connected to the light when + # home assistant tries to connect, this connection will fail. + await hass.async_add_executor_job(light.connect) + await hass.async_add_executor_job(light.get_color) + except pykulersky.PykulerskyException: + continue + # The light has successfully connected + hass.data[DOMAIN]["devices"].add(device["address"]) + async_add_entities([KulerskyLight(light)], update_before_add=True) + + # Start initial discovery + hass.async_add_job(discover) + + # Perform recurring discovery of new devices + async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + + +class KulerskyLight(LightEntity): + """Representation of an Kuler Sky Light.""" + + def __init__(self, light: pykulersky.Light): + """Initialize a Kuler Sky light.""" + self._light = light + self._hs_color = None + self._brightness = None + self._white_value = None + self._available = True + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove( + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + await self.hass.async_add_executor_job(self.disconnect) + + def disconnect(self, *args) -> None: + """Disconnect the underlying device.""" + self._light.disconnect() + + @property + def name(self): + """Return the display name of this light.""" + return self._light.name + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._light.address + + @property + def device_info(self): + """Device info for this light.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Brightech", + } + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_KULERSKY + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def is_on(self): + """Return true if light is on.""" + return self._brightness > 0 or self._white_value > 0 + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + default_hs = (0, 0) if self._hs_color is None else self._hs_color + hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + + default_brightness = 0 if self._brightness is None else self._brightness + brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) + + default_white_value = 255 if self._white_value is None else self._white_value + white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value) + + if brightness == 0 and white_value == 0 and not kwargs: + # If the light would be off, and no additional parameters were + # passed, just turn the light on full brightness. + brightness = 255 + white_value = 255 + + rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + + self._light.set_color(*rgb, white_value) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.set_color(0, 0, 0, 0) + + def update(self): + """Fetch new state data for this light.""" + try: + if not self._light.connected: + self._light.connect() + # pylint: disable=invalid-name + r, g, b, w = self._light.get_color() + except pykulersky.PykulerskyException as exc: + if self._available: + _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) + self._available = False + return + if not self._available: + _LOGGER.info("Reconnected to %s", self.entity_id) + self._available = True + + hsv = color_util.color_RGB_to_hsv(r, g, b) + self._hs_color = hsv[:2] + self._brightness = int(round((hsv[2] / 100) * 255)) + self._white_value = w diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json new file mode 100644 index 00000000000..4f445e4fc18 --- /dev/null +++ b/homeassistant/components/kulersky/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "kulersky", + "name": "Kuler Sky", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kulersky", + "requirements": [ + "pykulersky==0.4.0" + ], + "codeowners": [ + "@emlove" + ] +} diff --git a/homeassistant/components/kulersky/strings.json b/homeassistant/components/kulersky/strings.json new file mode 100644 index 00000000000..ad8f0f41ae7 --- /dev/null +++ b/homeassistant/components/kulersky/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/kulersky/translations/en.json b/homeassistant/components/kulersky/translations/en.json new file mode 100644 index 00000000000..f05becffed3 --- /dev/null +++ b/homeassistant/components/kulersky/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a8e871aa02e..833f11190b6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -108,6 +108,7 @@ FLOWS = [ "juicenet", "kodi", "konnected", + "kulersky", "life360", "lifx", "local_ip", diff --git a/requirements_all.txt b/requirements_all.txt index cb93fc3e7c9..725bc5f0c09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1475,6 +1475,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df7cbdafc63..c3f8ed3c3b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,6 +745,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.lastfm pylast==4.0.0 diff --git a/tests/components/kulersky/__init__.py b/tests/components/kulersky/__init__.py new file mode 100644 index 00000000000..2b723b28fbd --- /dev/null +++ b/tests/components/kulersky/__init__.py @@ -0,0 +1 @@ +"""Tests for the Kuler Sky integration.""" diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py new file mode 100644 index 00000000000..59e3188fd7e --- /dev/null +++ b/tests/components/kulersky/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Kuler Sky config flow.""" +import pykulersky + +from homeassistant import config_entries, setup +from homeassistant.components.kulersky.config_flow import DOMAIN + +from tests.async_mock import patch + + +async def test_flow_success(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Kuler Sky" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_no_devices_found(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_flow_exceptions_caught(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + side_effect=pykulersky.PykulerskyException("TEST"), + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py new file mode 100644 index 00000000000..1b2472d7d7f --- /dev/null +++ b/tests/components/kulersky/test_light.py @@ -0,0 +1,315 @@ +"""Test the Kuler Sky lights.""" +import asyncio + +import pykulersky +import pytest + +from homeassistant import setup +from homeassistant.components.kulersky.light import DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + SCAN_INTERVAL, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +import homeassistant.util.dt as dt_util + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +async def mock_entry(hass): + """Create a mock light entity.""" + return MockConfigEntry(domain=DOMAIN) + + +@pytest.fixture +async def mock_light(hass, mock_entry): + """Create a mock light entity.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + light, "get_color", return_value=(0, 0, 0, 0) + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_connect.called + light.connected = True + + yield light + + +async def test_init(hass, mock_light): + """Test platform setup.""" + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object(hass.loop, "stop"), patch.object( + mock_light, "disconnect" + ) as mock_disconnect: + await hass.async_stop() + await hass.async_block_till_done() + + assert mock_disconnect.called + + +async def test_discovery_lock(hass, mock_entry): + """Test discovery lock.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + discovery_finished = None + first_discovery_started = asyncio.Event() + + async def mock_discovery(*args): + """Block to simulate multiple discovery calls while one still running.""" + nonlocal discovery_finished + if discovery_finished: + first_discovery_started.set() + await discovery_finished.wait() + return [] + + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.light.async_track_time_interval", + ) as mock_track_time_interval: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + with patch.object( + hass, "async_add_executor_job", side_effect=mock_discovery + ) as mock_run_discovery: + discovery_coroutine = mock_track_time_interval.call_args[0][1] + + discovery_finished = asyncio.Event() + + # Schedule multiple discoveries + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + + # Wait until the first discovery call is blocked + await first_discovery_started.wait() + + # Unblock the first discovery + discovery_finished.set() + + # Flush the remaining jobs + await hass.async_block_till_done() + + # The discovery method should only have been called once + mock_run_discovery.assert_called_once() + + +async def test_discovery_connection_error(hass, mock_entry): + """Test that invalid devices are skipped.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object( + light, "connect", side_effect=pykulersky.PykulerskyException + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert entity was not added + state = hass.states.get("light.bedroom") + assert state is None + + +async def test_remove_entry(hass, mock_light, mock_entry): + """Test platform setup.""" + with patch.object(mock_light, "disconnect") as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + +async def test_update_exception(hass, mock_light): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException + ): + await hass.helpers.entity_component.async_update_entity("light.bedroom") + state = hass.states.get("light.bedroom") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_light_turn_on(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(255, 255, 255, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(255, 255, 255, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 50, 50, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 50, 50, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 45, 25, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(50, 45, 25, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(220, 201, 110, 180) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 45, 25, 180) + + +async def test_light_turn_off(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(0, 0, 0, 0) + ): + await hass.services.async_call( + "light", + "turn_off", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(0, 0, 0, 0) + + +async def test_light_update(hass, mock_light): + """Test KulerSkyLight update.""" + utcnow = dt_util.utcnow() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + # Test an exception during discovery + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST") + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_UNAVAILABLE + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object( + mock_light, + "get_color", + return_value=(80, 160, 200, 240), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_ON + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + ATTR_BRIGHTNESS: 200, + ATTR_HS_COLOR: (200, 60), + ATTR_RGB_COLOR: (102, 203, 255), + ATTR_WHITE_VALUE: 240, + ATTR_XY_COLOR: (0.184, 0.261), + } From 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 78a0d22878a..50482cb3e61 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 16623e4e704..1d90958efe6 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 f0a657d33d3..b38568210ad 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 a047d1e7477..b1469af787e 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 00000000000..27823ede2b7 --- /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 00000000000..5c62b1f53e6 --- /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 fc725b11adf..0c960a850e2 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 d28fe17a691..7d24da73e77 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 00000000000..b3d6b5d782e --- /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 00000000000..46de86e5ec1 --- /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 69d67c5b4f2..f1232b5f862 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 966e1a4065b..89563ff3533 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 b2f5ebd6e8e..129043728a5 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 7efa1e31765..5c0105a762e 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 f6f93fada05..185b0e031c5 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 5e57cd1464f..758c5c68bba 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 c5e0b6386b8..de00cf0fce6 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 57912d7d24d..a41161c7a6e 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -374,9 +374,11 @@ class IntesisAC(ClimateEntity): reconnect_minutes, ) # Schedule reconnection - async_call_later( - self.hass, reconnect_minutes * 60, self._controller.connect() - ) + + async def try_connect(_now): + await self._controller.connect() + + async_call_later(self.hass, reconnect_minutes * 60, try_connect) if self._controller.is_connected and not self._connected: # Connection has been restored From 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 c6deb8e9f8f..27614c3d49d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -404,6 +404,7 @@ homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn +homeassistant/components/slack/* @bachya homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smappee/* @bsmappee diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index ad45abbe3c0..e183dd455f1 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -3,5 +3,5 @@ "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], - "codeowners": [] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 88317b31585..90caad62a58 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -198,17 +198,21 @@ class SlackNotificationService(BaseNotificationService): _LOGGER.error("Error while uploading file message: %s", err) async def _async_send_text_only_message( - self, targets, message, title, blocks, username, icon + self, + targets, + message, + title, + *, + username=None, + icon=None, + blocks=None, ): """Send a text-only message.""" - message_dict = { - "blocks": blocks, - "link_names": True, - "text": message, - "username": username, - } + message_dict = {"link_names": True, "text": message} + + if username: + message_dict["username"] = username - icon = icon or self._icon if icon: if icon.lower().startswith(("http://", "https://")): icon_type = "url" @@ -217,6 +221,9 @@ class SlackNotificationService(BaseNotificationService): message_dict[f"icon_{icon_type}"] = icon + if blocks: + message_dict["blocks"] = blocks + tasks = { target: self._client.chat_postMessage(**message_dict, channel=target) for target in targets @@ -256,15 +263,15 @@ class SlackNotificationService(BaseNotificationService): elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: - blocks = {} + blocks = None return await self._async_send_text_only_message( targets, message, title, - blocks, username=data.get(ATTR_USERNAME, self._username), icon=data.get(ATTR_ICON, self._icon), + blocks=blocks, ) # Message Type 2: A message that uploads a remote file From 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 5c48de975f9..4fb21febb40 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 60d3867d05a..3d5af9b7c4d 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 f387e7bfda3..f0c191ac1a9 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 7ef0e3d6703..82d2f41de59 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 31e62a1a719..4047a8ef28c 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 8ab62019c3d..b62a98fd03f 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 9c2a1b1e371..7adf4b85b1a 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 96d52b57e85..e5ee9029302 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 67803ec56be..3f51a9c0884 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 ea46ea44846..3a7d26435ff 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 ad99f13e8f6..060188d65aa 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 3e1e1748737..98e3864e191 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 00000000000..611744e3191 --- /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 00000000000..654f87cbceb --- /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 c10d3691e6b..c11d22d974e 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -1,5 +1,6 @@ blueprint: name: Motion-activated Light + description: Turn on a light when motion is detected. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml input: @@ -17,7 +18,7 @@ blueprint: domain: light no_motion_wait: name: Wait time - description: Time to wait until the light should be turned off. + description: Time to leave the light on after last motion is detected. default: 120 selector: number: diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 9b79396f066..d3a70d773ee 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -1,5 +1,6 @@ blueprint: - name: Send notification when a person leaves a zone + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml input: @@ -26,6 +27,7 @@ trigger: variables: zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. zone_state: "{{ states[zone_entity].name }}" person_entity: !input person_entity person_name: "{{ states[person_entity].name }}" diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index bc40f76e7c2..524b04293ee 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -124,7 +124,9 @@ def _extract_blueprint_from_community_topic( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the topic") + raise HomeAssistantError( + "No valid blueprint found in the topic. Blueprint syntax blocks need to be marked as YAML or no syntax." + ) return ImportedBlueprint( f'{post["username"]}/{topic["slug"]}', block_content, blueprint @@ -204,7 +206,9 @@ async def fetch_blueprint_from_github_gist_url( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the gist") + raise HomeAssistantError( + "No valid blueprint found in the gist. The blueprint file needs to end with '.yaml'" + ) return ImportedBlueprint( f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint From 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 2b22fcdbd31..04f7719b8e6 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -25,5 +25,5 @@ async def _async_has_devices(hass) -> bool: config_entry_flow.register_discovery_flow( - DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 4c17d1bcba3..71dd4a158ca 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -33,6 +33,12 @@ DISCOVERY_INTERVAL = timedelta(seconds=60) PARALLEL_UPDATES = 0 +def check_light(light: pykulersky.Light): + """Attempt to connect to this light and read the color.""" + light.connect() + light.get_color() + + async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, @@ -69,13 +75,12 @@ async def async_setup_entry( for device in new_devices: light = pykulersky.Light(device["address"], device["name"]) try: - # Attempt to connect to this light and read the color. If the - # connection fails, either this is not a Kuler Sky light, or - # it's bluetooth connection is currently locked by another - # device. If the vendor's app is connected to the light when - # home assistant tries to connect, this connection will fail. - await hass.async_add_executor_job(light.connect) - await hass.async_add_executor_job(light.get_color) + # If the connection fails, either this is not a Kuler Sky + # light, or it's bluetooth connection is currently locked + # by another device. If the vendor's app is connected to + # the light when home assistant tries to connect, this + # connection will fail. + await hass.async_add_executor_job(check_light, light) except pykulersky.PykulerskyException: continue # The light has successfully connected @@ -83,7 +88,7 @@ async def async_setup_entry( async_add_entities([KulerskyLight(light)], update_before_add=True) # Start initial discovery - hass.async_add_job(discover) + hass.async_create_task(discover()) # Perform recurring discovery of new devices async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 1b2472d7d7f..5403f7cedde 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -56,11 +56,11 @@ async def mock_light(hass, mock_entry): ], ): with patch( - "homeassistant.components.kulersky.light.pykulersky.Light" - ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + "homeassistant.components.kulersky.light.pykulersky.Light", + return_value=light, + ), patch.object(light, "connect") as mock_connect, patch.object( light, "get_color", return_value=(0, 0, 0, 0) ): - mockdevice.return_value = light mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From 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 a8b9cb0ac4d..5a6ce5f5c64 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 2fd04943e4d..31a0bcd7762 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 6037bde5afd..58e090db20f 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 c0d50e18abc..1f46e7a17c6 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -279,7 +279,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except asyncio.CancelledError: pass - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client) + ozw_data[DATA_UNSUBSCRIBE].append( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client + ) + ) ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client else: From 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 33788960de3..258a75caa02 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 725bc5f0c09..3a906d83440 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 c3f8ed3c3b7..42d1319163b 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 34cd7edbf26..1bf6cdc580a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201202.0"], + "requirements": ["home-assistant-frontend==20201203.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f85297fc79..65f228f5a0c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3a906d83440..55fc295705d 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 42d1319163b..b100b4fe966 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 9ef9c74aa1a..a054d863c9e 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 d3e1db007f5..6725df44451 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 ebbceb69b0a..bed28b62975 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 00000000000..f8879f5715f --- /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 db6c097e1e3..ed5fa26f0c0 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 1e7d4d0f14e..2aeb94f66d2 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 b5c6863a94d..8306b2daf70 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 eae6626685d..9d1cd4210f4 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 16de4fd1039..aa0ac965a84 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 0d46a0f7643..8cbfd138257 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 f5b95a57f06..1a57bfbadf9 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 e8deb533de8..4d60b158c4c 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 913173f98cc..4bdc2959047 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 12395b1d078..d09fb725e34 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 ee630bc7a1b..a43e80d3629 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 a576b11e638..6312d4ecd18 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 51f0033b954..dab15def7b4 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 00000000000..412d31082f3 --- /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 00000000000..a11488ebca9 --- /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 00000000000..2fb8d234ea6 --- /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 5c62b1f53e6..2fbdd53c07c 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 00000000000..269e207e8a4 --- /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 fd2cb2181ac..853b498a51e 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 164e87a9642..b616437aa21 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 8b40a8edefc..11fe9ff88b3 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 07cc81cc0fd..1d7aaa7c74e 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 5d11c2e9a72..b84105745ac 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 5736c91714c..3d05dc82abc 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 cbb42aee925..af652c54509 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 eafe98f1541..53dc9ead653 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 8781b90c3d8..2e0864c9f72 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 79dc4c81b2a..d8208e6ce4e 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 7ada76c1d21..3fefe08f98b 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 07463bc8a15..c53ffd83279 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 91a0dc60be7..90c98e491df 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 07e7748e133..19f8f40b9d5 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 e84966b8d53..1be70def034 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 f52e877a9d4..bc955f119e9 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 03f9cb3cfbc..42278561d58 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 1949bd98b26..b1a19792a08 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 f6695fc2af9..335aa73a67c 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 fab16780a57..1aaa5b04072 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 e80c32afd01..b0e44bedac4 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 f0b6c3eade4..989db1c2564 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 ab790dafe9b..4584a383136 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 9be7ac31e60..e19ff18b364 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 a4b3bd2fd86..bb1d109bb80 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 e35c96a7bf7..cbbc3dc8f53 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 855cefaa774..ce7a1201223 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 5da4b6d7c09..73083d2b735 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 54cad2049fd..e9789c855d0 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 e25b4cd7c8f..8f301b73b3e 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 8c4ac5a0d73..ee877f78967 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 bc51c7f0bbc..6000b968e5e 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 a60ef4fdebf..4e719a7957f 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 8bf65ef6ee6..cad7d736a9d 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 88e8628848c..0ac0bac013b 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 608c5bbcba7..734498585f3 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 b09eac77619..7b85df577ef 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 0bef632c5a8..4ffe6730453 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 0054edbdb0d..d81ca02f6ba 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 b033c16afb6..5c25a8cb98c 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 324a92cd8b5..9c5448266e6 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 91a0dc60be7..90c98e491df 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 e40cef4f940..bf3a1606e6e 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 4ab79dd603a..608a2150c61 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 95b4a8a8a61..0d74360b8f3 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 95ddf7e51a7..fe3e8fb7b43 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 56ad5128d97..e08a2c5f6df 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 8bf65ef6ee6..cad7d736a9d 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 e3e9247e7c4..0f1093f5b5b 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 75c46125c14..6490904a32e 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 21c896182fd..066ce89c2b2 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 f0ea5d28ffb..c8b067c887c 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 9a5c0b2b54f..ffb2b3a0e50 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 85d167fb040..e78e05855c9 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 a965fa38816..df1eb910d23 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 00000000000..d93018f8a3c --- /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 3923f95f71b..aaf1800f748 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 3a6f1b64fa9..1c16db77faf 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 cecd2aea7e7..e4d2c8f1488 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 beef1c7070e..fe5b80f72f1 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 923fdbfb449..d2f73fca37b 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 5358cccb85b..dd69e0ec7c4 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 ea5e5afce21..aceb4ea78d5 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 7e0da1fb844..cd5d576d0ad 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 9fcb91c4627..f5d4446def5 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 b94faab25c0..ea7a2c4f9b2 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 43b43661b6d..9ab55c19a78 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 f49de8669d1..363e62a1b5f 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 a4aaf909344..11d962f9d15 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 e4b38a6d109..604dc28b571 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 00000000000..d3f0e37a132 --- /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 00000000000..9e7bb472e0d --- /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 46de86e5ec1..85a42bf1be5 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 00000000000..90c98e491df --- /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 ed704711a66..154e82ec301 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 d0238ff7436..b14abdd6b62 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 65dc4ff8da7..8c2dcdb53ed 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 15c253dd4d4..b5a1552efc2 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 a30b7b2518b..5807cdd7c16 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 ab4e0832ed6..4e8df0d5e9f 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 19c41241a5e..508a652ce9b 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 0675ede61bd..6c3049eff01 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 e653bda9205..b54a6783980 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 8c8d23b565b..37925ca6288 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 586d96a78ac..53d6d391e8f 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 de92aee3171..bfb27361889 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 2c5c8c61fef..602b3d47dcf 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 bd72b659ae7..931b8aa770e 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 80d0f8ee66a..b097aec56af 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 590082b826a..eab1d9741ad 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 588675c670e..e396deabb68 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 34450afc84b..0dc0931afe5 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 5066f5a2edb..7b480bcc0f7 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 86e059df4ff..4b6597725ef 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 12bc209815f..865bd1dbd08 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 eac51c4cf8f..d04a5b165b1 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 b75bd37958f..7c65e836f9e 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 cc7d8071c2c..f5f1bfd39ed 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 7858092db4f..26fd5f34117 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 1127dc941ea..5189795ec9c 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 99b5a46570e..c2c39e00d68 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 8b2702b6708..ae155ccf2c2 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 acafb6eee4b..9c606534a5b 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 6541d8accde..b21982fede8 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 35099f2a59b..ea138287c78 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 cf9f264a618..daa12f9e569 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 f557a83009c..43f456f7574 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 b89f1472478..6c92b557797 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 621e48bab7e..b9cc99e72be 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 f4334e1d632..f9ed5469da0 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 5ac554d2694..1b39556f451 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 dbfe2075e2d..aec745ea38b 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 bbab02f959e..710d363f771 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 3841a173df9..93a99ba1d31 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 8cfa8bdb46b..45edbf2d88e 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 85797ab2082..c7d73c344d8 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 13a968114a5..815ee581e69 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 ddfcbef493f..77bfa7bfdb1 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 f8dbde46919..b800daee779 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 cefc44956c7..9b5829cf209 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 7ce887b05c2..b40bc6d1971 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 827def7f302..3da2e5f5384 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 b9a66540c3b..5eb31bfa792 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 9509ace6546..c76871bcecd 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 94e6d6cb489..cfa3a4aa3b4 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 794c67454fb..932e5cadd75 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 00b152205f3..f34bce445f7 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 37dbb151d8e..05cdeb6852b 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 8bf65ef6ee6..cad7d736a9d 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 e932e18a2b5..00b442399c1 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 356e58f640b..d819bfd4bbd 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 a63efaf6dc2..b73a2e57f1a 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 59ac0f5cccb..bf0150523b3 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 4996f9dea9c..37a10c69ec6 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 77f726a0dcd..4f41b5a1e56 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 df467dd38b9..d232b491b68 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 30951f88d0d..35952af999b 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 01c1db919cb..18cf04cf5a5 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 b8f53a74ff3..b97772a8d46 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 16659304045..3dfb1649557 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 4768da884da..71390930e35 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 b73aaade30c..ddb334d7545 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 b47280e3a9e..31a9d3ce950 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 e9459bf08f5..e88b4ec3923 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 96b9ad519d8..ce15c28f47b 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 00000000000..19a6909de48 --- /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 85942f812b0..067374f6c10 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 f8cb25f7df5..87bf347795c 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 a31ea74fb0b..fbbc85c4a1e 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 4f50be27955..d5e78faf91c 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 59e2d80c561..9126e0e4ea4 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 1431fc8e1b7..477eb0ffa9c 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 daf5ff0ec18..46f6f6cf162 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 e88d982b8a1..2fac2ac142d 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 71d22d66cb0..2204e7c3323 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 21b232b757f..9a48c1bc525 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 fc75254a9e2..5329ceb31ec 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 52f502b546f..6031af445a6 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 f2fb4c0926c..3735d12e616 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 630afb02973..0776d7cb0e5 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 a325d458acb..7e6a113e1e0 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 b17ad0b5511..d87f8cf51e0 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 e4809c9b63a..b121c005fa7 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 008b007e2fe..64423efed3e 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 28469cd1d93..f9bbe99d9ce 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 96950484588..4afb098caa6 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 7293ce97611..b8d7031ee12 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 02cffeefc44..264ad237af1 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 abbc12e6d8f..b266e25b39c 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 74d6a858d84..257ed829b6a 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 48f3ad6d172..f5573973728 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 0b9966135b1..4be83508478 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 ae2956cc5e9..ea02e179337 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 8859e831d57..0a86501c8f6 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 394a42c5fd6..cd917f42b47 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 37c74d07f51..0073bb22484 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 13eb90b55db..2a0dbc2544e 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 477bd13374c..07fc710408f 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 cd1059436d1..582aea354c6 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 fd77eb4df82..95499fb7b82 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 b19ebdb40f8..d9bf3c123b4 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 91a0dc60be7..90c98e491df 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 e62d61ac8ea..65825074313 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 fdb263fd5f7..f5c07a9efc9 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 a4d6ec6036f..a4c6f77fc13 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.2"], + "requirements": ["hatasmota==0.1.4"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 55fc295705d..35703034d35 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 b100b4fe966..e5552f4c45b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 627eb5198aa..f09c27da753 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -493,6 +493,191 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_OFF +async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["so"]["17"] = 0 # Hex color in state updates + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 255, 128) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + +async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["ty"] = 1 # Tuya device + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -745,6 +930,26 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->100: Speed should be capped at 40 + await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->0: Speed should be 1 + await common.async_turn_on(hass, "light.test", brightness=0, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 1;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->50: Speed should be 4*2*2=16 await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( From 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 20f9e82dcb0..8d7d856e50c 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/aurora", "config_flow": true, "codeowners": ["@djtimca"], - "requirements": ["auroranoaa==0.0.1"] + "requirements": ["auroranoaa==0.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35703034d35..59ef44f8fa4 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 e5552f4c45b..472941a3da7 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 8ec5a611f1d..e4553a2498b 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 48d13a716ab..2638477ef79 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 71bcbedda88..f378a1fc2a1 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 86b905f2b0f..82a88de918b 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 fa390f9bf3b..d8fb9f2914b 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 4543bc27984..887560b154e 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -147,9 +147,10 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.network_key = user_input[CONF_NETWORK_KEY] self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = {CONF_ADDON_DEVICE: self.usb_path} - if self.network_key: - new_addon_config[CONF_ADDON_NETWORK_KEY] = self.network_key + new_addon_config = { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + } if new_addon_config != self.addon_config: await self._async_set_addon_config(new_addon_config) From 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 887560b154e..7c7c6e65dfe 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -58,17 +58,14 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - addon_config = await self._async_get_addon_config() - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return await self.async_step_hassio_confirm() async def async_step_hassio_confirm(self, user_input=None): """Confirm the add-on discovery.""" if user_input is not None: - self.use_addon = True - return self._async_create_entry_from_vars() + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) return self.async_show_form(step_id="hassio_confirm") @@ -107,6 +104,9 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True if await self._async_is_addon_running(): + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") return self._async_create_entry_from_vars() if await self._async_is_addon_installed(): diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 289b6c7f4cd..e86232adc65 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -159,9 +159,10 @@ async def test_not_addon(hass, supervisor): assert len(mock_setup_entry.mock_calls) == 1 -async def test_addon_running(hass, supervisor, addon_running): +async def test_addon_running(hass, supervisor, addon_running, addon_options): """Test add-on already running on Supervisor.""" - hass.config.components.add("mqtt") + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -182,8 +183,8 @@ async def test_addon_running(hass, supervisor, addon_running): assert result["type"] == "create_entry" assert result["title"] == TITLE assert result["data"] == { - "usb_path": None, - "network_key": None, + "usb_path": "/test", + "network_key": "abc123", "use_addon": True, "integration_created_addon": False, } @@ -193,7 +194,6 @@ async def test_addon_running(hass, supervisor, addon_running): async def test_addon_info_failure(hass, supervisor, addon_info): """Test add-on info failure.""" - hass.config.components.add("mqtt") addon_info.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -213,7 +213,6 @@ async def test_addon_installed( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on already installed but not running on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -250,7 +249,6 @@ async def test_set_addon_config_failure( hass, supervisor, addon_installed, addon_options, set_addon_options ): """Test add-on set config failure.""" - hass.config.components.add("mqtt") set_addon_options.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -273,7 +271,6 @@ async def test_start_addon_failure( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on start failure.""" - hass.config.components.add("mqtt") start_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -302,7 +299,6 @@ async def test_addon_not_installed( start_addon, ): """Test add-on not installed.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None await setup.async_setup_component(hass, "persistent_notification", {}) @@ -348,7 +344,6 @@ async def test_addon_not_installed( async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): """Test add-on install failure.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -488,3 +483,54 @@ async def test_abort_discovery_with_existing_entry( assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_discovery_addon_not_running( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test discovery with add-on already installed but not running.""" + addon_options["device"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "start_addon" + assert result["type"] == "form" + + +async def test_discovery_addon_not_installed( + hass, supervisor, addon_installed, install_addon, addon_options +): + """Test discovery with add-on not installed.""" + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "install_addon" + assert result["type"] == "progress" + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" From 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 1bf6cdc580a..65fe745e51d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201203.0"], + "requirements": ["home-assistant-frontend==20201204.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 65f228f5a0c..d820f5e715b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 59ef44f8fa4..1148222aaff 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 472941a3da7..e451e137dc4 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 ba95a0e4bc6..48f35e035f0 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 c6019c10843..2dbd1629487 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 0760427d46b..0326f18ac69 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 dc06d01e596..fa4883ae5a2 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 03812be0548..5b3a4778fcd 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 ac832aacc61..f8f04414fa1 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 16223582c33..c8827e20e01 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 e37987bc821..7c600d98401 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 792b9413294..0bd7159cf97 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 5d4522e8ce1..aa24b5ff975 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 1c22344f551..86aaa213a15 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 e1a9306e004..4d9689ccf5f 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 2ac9aabfc91..c6e4fb3ba82 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 1d90958efe6..6a675c17248 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 4fb6a0905cc..a0ed36a7169 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 6f3fd919df5..bf6d7a461ce 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 46eb3a91f04..dd24ee3066f 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 f13c212e25a..e36e6f86ec7 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 412d31082f3..59f83e02728 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 00000000000..d03a77ca1c2 --- /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 00000000000..3a4ae887aae --- /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 00000000000..bbfc9f7b311 --- /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 27823ede2b7..974fe2ce5f4 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 2fb8d234ea6..e8950d1c714 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 2fbdd53c07c..e3f5804cebe 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 1f27531f769..3eb7feec614 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 00000000000..0510da2305e --- /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 467cc64f561..4dd8ddda76b 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 00000000000..520df7ee4cd --- /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 00000000000..0278fe07bfe --- /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 00000000000..a8ee3fa57ac --- /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 f946a859ea2..d08fcdc7fe0 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 8ff6dfb982e..f5ba52b1a53 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 00e949b652b..4db72851aa3 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 b9cc99e72be..4ba465a3146 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 60d64f9afbf..f06c2896bc8 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 e03c71ae709..ff3e0a711c5 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 a143163ca1b..c9fd17c59bd 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 129043728a5..07dc84eae07 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 2ab4bc127c4..b45ca4497a4 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 4f9b559bc19..344ea569104 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 1148222aaff..da06aa3cd2d 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 e451e137dc4..36c65ae0ffe 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 871f91d447c..14756f1183c 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 199dcf425de..e5bd7ea1ca8 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 89dccf6c31e..3199b89f21c 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 1841dbbc438..f69c94f0f5e 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -44,6 +44,11 @@ class BlinkBinarySensor(BinarySensorEntity): """Return the name of the blink sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + @property def device_class(self): """Return the class of this device.""" From 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 dc41bb778ec..881ea883c4c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -121,9 +121,11 @@ class DeconzGateway: async_dispatcher_send(self.hass, self.signal_reachable, True) @callback - def async_add_device_callback(self, device_type, device=None) -> None: + def async_add_device_callback( + self, device_type, device=None, force: bool = False + ) -> None: """Handle event of new device creation in deCONZ.""" - if not self.option_allow_new_devices: + if not force and not self.option_allow_new_devices: return args = [] diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 2c286fac0a1..d524354ff0b 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -146,10 +146,10 @@ async def async_refresh_devices_service(hass, data): await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP) - gateway.async_add_device_callback(NEW_LIGHT) - gateway.async_add_device_callback(NEW_SCENE) - gateway.async_add_device_callback(NEW_SENSOR) + gateway.async_add_device_callback(NEW_GROUP, force=True) + gateway.async_add_device_callback(NEW_LIGHT, force=True) + gateway.async_add_device_callback(NEW_SCENE, force=True) + gateway.async_add_device_callback(NEW_SENSOR, force=True) async def async_remove_orphaned_entries_service(hass, data): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 5038c5bf3f2..78a4f1e937d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -10,15 +10,19 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, + CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SENSORS = { "1": { "id": "Presence sensor id", @@ -172,7 +176,7 @@ async def test_add_new_binary_sensor_ignored(hass): """Test that adding a new binary sensor is not allowed.""" config_entry = await setup_deconz_integration( hass, - options={CONF_ALLOW_NEW_DEVICES: False}, + options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 @@ -188,8 +192,23 @@ async def test_add_new_binary_sensor_ignored(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + assert not hass.states.get("binary_sensor.presence_sensor") entity_registry = await hass.helpers.entity_registry.async_get_registry() assert ( len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 ) + + with patch( + "pydeconz.DeconzSession.request", + return_value={ + "groups": {}, + "lights": {}, + "sensors": {"1": deepcopy(SENSORS["1"])}, + }, + ): + await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("binary_sensor.presence_sensor") From 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 faba23a52b9..fa18c0e5784 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 7b4cedfebad..5d712045c93 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 8b0f4951bf9..e3eb92a426f 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 ae88441f89e..c5e407573ba 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 f4545817c9f..18342aa1d98 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 def025e0cf2..c6ef895b7df 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 cac13ee1653..ed211473e29 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 00000000000..1cc51f400da --- /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 ddf7e61a3f6..26b54def974 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 1d6f7cb6df4..5891629627e 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 ccfddca4b03..94396bbf883 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate.const import ( SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) @@ -161,6 +162,7 @@ SUPPORT_FLAGS = ( | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE + | SUPPORT_TARGET_HUMIDITY ) @@ -651,7 +653,7 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity): """Set the humidity level.""" - self.data.ecobee.set_humidity(self.thermostat_index, humidity) + self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" From 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 ef3f1224a4b..e4450e7a306 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 9a357d76eb7..600493e8a12 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 afae1b661ab..e0c31d38bbb 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 f8f04414fa1..1647c5ce52d 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 9983967f764..b25d1c1aa39 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 b12b9249373..65be13fd96c 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 59f83e02728..98169067fba 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 974fe2ce5f4..88a7c986152 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 d46f12af511..550da4d38ec 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.6.0"], + "requirements": ["ring_doorbell==0.6.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index da06aa3cd2d..9e599f62c26 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 36c65ae0ffe..9f9775cbd91 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 ed2d3c74d7d..042bc4771c1 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 c1a956f3f4d..d05e36ed0eb 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 fe7b0c7783f..a49effdb75d 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 38156354cda..d3f852d7a49 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 0df67a94182..cc0e1ef9712 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 d7d7d6e9b34..ea954b9f0db 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 084965331e5..14260bd572d 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 00000000000..8fcd9c13425 --- /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 6f56d996b63..2ae53598eb4 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 00000000000..37e27fd8394 --- /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 ce7cbc3f548..fa5aa3de317 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 86d678d31af..8b35399bcb0 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 ae35beabf6b..7a80a1b3509 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 f7830dbe18b..d6732cdddcf 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 00000000000..8d6cb9a2ebf --- /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 94dfbefb86a..fa5dc98c8fa 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 00000000000..bb9215f0393 --- /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 98169067fba..0314afa1f53 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 00000000000..486ff0c51e4 --- /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 fdeb639b12b..097e3d086d6 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 d34bb36bc00..16752dd0071 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 5560383b710..45461329fc2 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 aad75b3bed0..336f6ac5f68 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 a637b68b0ba..baf9ce33cdd 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 b7cbb547b0f..21754867038 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 50d5409ef8d..2af1f548447 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 cedcdc27327..9d7fdda1006 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 b7fc26165a0..5a8ebbeea05 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 188effb27af..ed650faa027 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 9d37ff831d2..5e5f1367f58 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 bf246b55b36..45fb03ad040 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 5e4c740d66f..d62513ec255 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 f681da4210f..47b39d574b5 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 00000000000..e328e4f580b --- /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 af42481b251..0e2e7eeb9cc 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 00000000000..158cd3f3f74 --- /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 ce7cbc3f548..f13cad90edc 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 617aed245e0..dd9b538ae8b 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 ce6d6df8906..b33812fa24d 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 34a23569b96..4a43bae51b2 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 b8a454fbaba..ca6b9a6542c 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 af953a1caaa..8af2ff4345a 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 09ab0e6711c..56c91431f13 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 3f200f4109e..db021f4afcf 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 ce7cbc3f548..188c4a2a6de 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 20bba0ede4b..f6e4d5f5dc4 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 c4d1cc35cc1..99a658c7a0e 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 8c9b894c4b7..479685ff7e8 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 00000000000..352e98916f1 --- /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 e010af99a0b..6ff4d786447 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 00000000000..c78c9a5aba5 --- /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 1e3d9138c84..4a071063d47 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 c7081cd694a..83724bb7d05 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 8d3dfe38d4d..3ad6eaad7ea 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 09e13bc2007..3cd69cf5ddf 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 a5b5cd26dc2..9d2eadee61c 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 4e202834624..11e5023bae9 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 98180e11248..88f4021b4ab 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 00000000000..286cd58dd89 --- /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 f7195cd0bff..d5f64d59f9f 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 00000000000..5ce246347c5 --- /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 8602afcabfe..47e4e6e3831 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 00000000000..e25888655a9 --- /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 0077ceddd46..b2fce54d6b1 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 2a9c91681be..04374af8e82 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 973601e744a..06083bae759 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 ce7cbc3f548..a8931048295 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 00000000000..462b746816a --- /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 7bf340567f5..c16c2c3baa4 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 00000000000..b5da3fdfc97 --- /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 deeb376e5e0..c4ab5c1e636 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 81a8804ee40..34d057a9ab0 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 eef0cc82c15..f573f0645bc 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 8b7889f0d12..ef5279e090a 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 00000000000..ac9710c6b9b --- /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 24825307e76..3b466866334 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 420196bb050..3a3a13b91ce 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 eaed455b71a..030af8e090b 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 3f25486c6a4..a9ebd3c23ec 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 02353e5fca5..eccf48139bd 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 00000000000..d252c078a2c --- /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 00000000000..25538aa0036 --- /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 b8a454fbaba..fe440913178 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 0c5c7760566..db82206819d 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 ccfb08328fb..157c61cf243 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 972aed55cc4..19ddbf6057c 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 00000000000..e25888655a9 --- /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 9c848bd8ec8..71370e40068 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 00000000000..ef31de296c4 --- /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 6ca0b0b1948..93575068121 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 00000000000..9a0e75c24fa --- /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 1f8cb29ff0f..5811494fffb 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 2a193a19f96..614256fc701 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 6641658bd48..2cfc8af2d6d 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 00000000000..7a8b164e32b --- /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 77ce7025f70..b177591d4e6 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 b8a454fbaba..f1e1a2f9fe3 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 bfef7be3f11..9ca512ca53a 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 00000000000..fe188057e46 --- /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 2f0e67a9772..325e8dde098 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 4a071063d47..b47060ac87c 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 b4642359973..809cbc360ae 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 79c4276c23b..6da647ac29b 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 f9199091a85..afab5e616ef 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 4a071063d47..7953cf5625c 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 657ce03e544..f2766f5a2c0 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 24825307e76..c0a3dae8aae 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 00000000000..3e10b977773 --- /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 00000000000..bd1e14729e0 --- /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 cfc92a512d4..c3662a032a6 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 960e3a9cf5c..4285ee45c87 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 f736ec7e3cc..aabac4f4cfe 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 3fbf1797b31..7015a44b5f9 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 dbc8db55d63..ebc78e1346b 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 00000000000..75d85097874 --- /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 1e4f4cadc23..9a5c19acf10 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 f681da4210f..6e0b2481c8c 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 00000000000..3d0630027a8 --- /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 81b70bcd082..3b63ab169e2 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 808e3f3f7ea..65283f66f49 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 1ccfdddc1d1..401e10c256a 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 0c5c7760566..c748619963b 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 00000000000..c299020ce9a --- /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 82e6756df11..072d1ea0565 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 a8b6c3c6ccf..51ea84f7c2f 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 4c01137c496..a5648a9138e 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 e6c1baf1ca6..99cd4dba4e5 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 57e7ea502f5..31e872a71cd 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 ce8a9287272..335e097e998 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 00000000000..9890048c368 --- /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 00000000000..561c8d77287 --- /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 5ce246347c5..8493f660318 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 ad96aeba53c..28a2d603dd0 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 565b9f6c0e8..dfae15e9686 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 b02332eb0b0..d66cc0e5dd9 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 a208b49150a..9b5df6cf934 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 aba871acf6b..54f841b2245 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 2933743c867..7430480cc09 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 efab29fd698..9f2ed5a4b90 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 01078bbddfe..52bbd75e9bf 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 00000000000..28b7a920e93 --- /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 ce7cbc3f548..43b4c5d49b0 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 00000000000..ce8a9287272 --- /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 00000000000..c299020ce9a --- /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 b459d4e6bfd..f3da93ad961 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 00000000000..3e10b977773 --- /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 4264b1e4a08..dcc4132df7f 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 00000000000..3df19f11fa9 --- /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 6030c972448..e8abc7c8656 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 0df67a94182..c249c325adc 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 23f4662a4c1..bed3f76be0b 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 e266cf06266..e7e5968a6d9 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 9ecaef216f9..189f0c1b2f6 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 d399370847c..3c17682089a 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 00000000000..3d0630027a8 --- /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 a68c7635501..c311dbf9eff 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 b8a454fbaba..ae7ec8fa5e9 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 00000000000..451ff82e74b --- /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 a5a1d76bfbb..997757d2bc6 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 00000000000..abed97c8933 --- /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 0c5c7760566..ae100e45845 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 00000000000..a2f32087684 --- /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 5cf1a0dcd00..fb4e459281c 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 ce7cbc3f548..c30760a9a8f 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 b1a4f0d7b36..dce6f5934d1 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 00000000000..2278e7701aa --- /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 00000000000..0077ceddd46 --- /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 b80d6630c35..83caf877b92 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 a6e5cd46cbb..849cb1588f7 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 7953cf5625c..308f60400aa 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 00000000000..38f070ab3db --- /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 5c127b797e7..cd1c9b7978e 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 4ede7753310..8bab9bd19b1 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 e4a8cc8062f..6d350188989 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 00000000000..1424e35f359 --- /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 00000000000..f8fa0efe967 --- /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 49be02c195c..74942081884 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 3b715eed0dd..09a3235377d 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 bc8cf1defc0..be740b13fb5 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 a09808316c4..aee84f9fe2b 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 751ef106147..ad9580f34ed 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 3a1f2eb9f7a..0c70ad3c9fc 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 d820f5e715b..1818385c0e2 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 ece1877ea75..cbe339fd835 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 d5d133d4a3a..c9acb4d82d7 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 4d049753f43..0a9c1dc6104 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 a1924296f75..45b73cf58ee 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 e06d3b881f7..4879842ee22 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 3eeac8bd8dd..d2fe23a8112 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 9e599f62c26..65092ac608c 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 ce80aa8786a..ef2fec9f84f 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 00000000000..391a8c1affe --- /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 00000000000..c58b8b12b53 --- /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 af82db0ffbb..c42a53b2dae 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 1796847bd74..75ac015b83b 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 59e1b0754c0..0be89af4810 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 3c34da574a6..298c7e111b2 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -158,7 +158,11 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): last_event_count = self._last_input_events_count.get(channel) self._last_input_events_count[channel] = block.inputEventCnt - if last_event_count == block.inputEventCnt or event_type == "": + if ( + last_event_count is None + or last_event_count == block.inputEventCnt + or event_type == "" + ): continue if event_type in INPUTS_EVENTS_DICT: From 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 bfbaa828ea7..0055d323938 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 0cd498b4e3b..e3e59676bf5 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -425,22 +425,21 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): self.data = {} self.attributes = {} self.unit = energy_details["unit"] - meters = energy_details["meters"] - for entity in meters: - for key, data in entity.items(): - if key == "type" and data in [ - "Production", - "SelfConsumption", - "FeedIn", - "Purchased", - "Consumption", - ]: - energy_type = data - if key == "values": - for row in data: - self.data[energy_type] = row["value"] - self.attributes[energy_type] = {"date": row["date"]} + for meter in energy_details["meters"]: + if "type" not in meter or "values" not in meter: + continue + if meter["type"] not in [ + "Production", + "SelfConsumption", + "FeedIn", + "Purchased", + "Consumption", + ]: + continue + if len(meter["values"][0]) == 2: + self.data[meter["type"]] = meter["values"][0]["value"] + self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} _LOGGER.debug( "Updated SolarEdge energy details: %s, %s", self.data, self.attributes From 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 15ca58a525f..aa13bee81d0 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -29,7 +29,7 @@ class _SensorTypes(enum.Enum): UPLOAD_SPEED = "upload_speed" @property - def unit(self) -> Optional[str]: + def unit_of_measurement(self) -> Optional[str]: """Return a string with the unit of the sensortype.""" if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD): return DATA_GIGABYTES @@ -161,3 +161,8 @@ class AsuswrtSensor(CoordinatorEntity): def icon(self) -> Optional[str]: """Return the icon to use in the frontend.""" return self._type.icon + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + return self._type.unit_of_measurement From 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 fa18c0e5784..72f11b7b005 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 baa318f891f..d7d8acf4f29 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 4072c43bc27..175ee8f1d5b 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, callback +from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -207,7 +207,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ) @callback - def _async_startup(event): + def _async_startup(*_): """Init on startup.""" sensor_state = self.hass.states.get(self.sensor_entity_id) if sensor_state and sensor_state.state not in ( @@ -215,8 +215,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity): STATE_UNKNOWN, ): self._async_update_temp(sensor_state) + self.async_write_ha_state() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + if self.hass.state == CoreState.running: + _async_startup() + else: + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index eaf7c8e5651..71c6f41282b 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -209,6 +209,30 @@ async def test_setup_defaults_to_unknown(hass): assert HVAC_MODE_OFF == hass.states.get(ENTITY).state +async def test_setup_gets_current_temp_from_sensor(hass): + """Test that current temperature is updated on entity addition.""" + hass.config.units = METRIC_SYSTEM + _setup_sensor(hass, 18) + await hass.async_block_till_done() + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_temp": 16, + } + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY).attributes["current_temperature"] == 18 + + async def test_default_setup_params(hass, setup_comp_2): """Test the setup with default parameters.""" state = hass.states.get(ENTITY) From 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 00000000000..03574e6554b --- /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 00000000000..aa45f8f92b2 --- /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 bcc969f0354..520bced2385 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 d1e64d44bbc..ca576f83d2a 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 00000000000..4ee6986e599 --- /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 9881ef15dcb..71c8005cbc5 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -21,6 +21,14 @@ HOMEKIT_BRIDGE_DOMAIN = "homekit" HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" +HOMEKIT_IGNORE = [ + # eufy Indoor Cam 2K Pan & Tilt + # https://github.com/home-assistant/core/issues/42307 + "T8410", + # Hive Hub - vendor does not give user a pairing code + "HHKBridge1,1", +] + PAIRING_FILE = "pairing.json" MDNS_SUFFIX = "._hap._tcp.local." @@ -255,6 +263,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. + if model in HOMEKIT_IGNORE: + return self.async_abort(reason="ignored_model") + + # If this is a HomeKit bridge exported by *this* HA instance ignore it. if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a8eb869abf4..72a8133159d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -257,6 +257,21 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info["properties"]["md"] = "HHKBridge1,1" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + +async def test_discovery_ignored_hk_bridge(hass, controller): + """Already paired.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") From 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 b5ef3dc528b..44031e846c3 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 1bc477277c9..c7325f776fa 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 6aac2be6dda..5d4aa18ee2b 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 fc00d4ea8b5..e2210d0279a 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 80412d95fb7..da3037f720d 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 0e3d53be081..ee3471725b6 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.10"], + "requirements": ["pymyq==2.0.11"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 65092ac608c..1e326de8026 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 9f9775cbd91..3b45bb79381 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 943e1b33199..721fb36fd36 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 9d7c3057939..2ef904f8680 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 480d53aa454..815a8601314 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 0f209b366e8..eeb37b46df9 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.0"], + "requirements": ["simplisafe-python==9.6.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e326de8026..e2d87b8b1a6 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 3b45bb79381..bfb3abbaca6 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 12bca8378c3..57aaa2b0b01 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 3d5af9b7c4d..29a06c558fe 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 ce8b44f5702..a8df0107eeb 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 f0c191ac1a9..1909ff818b9 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 30ca5165c85..9f045694460 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 72f9f94c210..9047ed9095f 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 e2d87b8b1a6..4eddfe6dc75 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 bfb3abbaca6..92c40967366 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 420977cd40c..c3922666665 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 82d2f41de59..a35576ed353 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 4047a8ef28c..0e69a67cdbf 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 b62a98fd03f..b4dbd0d140e 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 034ca33aece..f2b3dfba32c 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 7adf4b85b1a..beb7e42400f 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 abc49b1f5a0..86f05b8987e 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 163407ffce6..2d0906f73ab 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 d80e3bcae1e..038cbdc20a3 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 1b35ab98993..8bc909e2bab 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 826cfc22d4d..68d6ee44b0b 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 00000000000..72430b5e15c --- /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 4b8668e08a0..d72bc9bc015 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 981cb51c83a..fca08d49d7c 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 00000000000..a4f73279a76 --- /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 00000000000..7d765f80f8d --- /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 bb070f391a7..4e857279e97 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 00000000000..861ee57283c --- /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 780bb5bc999..f1821c9e480 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,13 +6,14 @@ "requirements": [ "bellows==0.21.0", "pyserial==3.4", - "zha-quirks==0.0.47", + "pyserial-asyncio==0.4", + "zha-quirks==0.0.48", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.28.1", + "zigpy==0.28.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.2.2" + "zigpy-znp==0.3.0" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4eddfe6dc75..e6b68edcaca 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 92c40967366..781832d82bc 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 eeb37b46df9..a502a7908f0 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.1"], + "requirements": ["simplisafe-python==9.6.2"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6b68edcaca..bd7e3474308 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 781832d82bc..b15a2d7afce 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 cbb3d2a70cb..5aecde16327 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -136,6 +136,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", From 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 bc665baeb86..5876331ea97 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -6,6 +6,7 @@ import logging from tuyaha import TuyaApi from tuyaha.tuyaapi import ( TuyaAPIException, + TuyaAPIRateLimitException, TuyaFrequentlyInvokeException, TuyaNetException, TuyaServerException, @@ -137,6 +138,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) as exc: raise ConfigEntryNotReady() from exc + except TuyaAPIRateLimitException as exc: + _LOGGER.error("Tuya login rate limited") + raise ConfigEntryNotReady() from exc + except TuyaAPIException as exc: _LOGGER.error( "Connection error during integration setup. Error: %s", diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index e2048aaf7bf..5d22a83e03e 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -2,7 +2,12 @@ import logging from tuyaha import TuyaApi -from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException +from tuyaha.tuyaapi import ( + TuyaAPIException, + TuyaAPIRateLimitException, + TuyaNetException, + TuyaServerException, +) import voluptuous as vol from homeassistant import config_entries @@ -103,7 +108,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): tuya.init( self._username, self._password, self._country_code, self._platform ) - except (TuyaNetException, TuyaServerException): + except (TuyaAPIRateLimitException, TuyaNetException, TuyaServerException): return RESULT_CONN_ERROR except TuyaAPIException: return RESULT_AUTH_FAILED @@ -249,6 +254,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + + if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + _LOGGER.error("Tuya integration not yet loaded") + return self.async_abort(reason="cannot_connect") + if user_input is not None: dev_ids = user_input.get(CONF_LIST_DEVICES) if dev_ids: diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 642a4dbe5d1..7481e56f00a 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuyaha==0.0.8"], + "requirements": ["tuyaha==0.0.9"], "codeowners": ["@ollo69"], "config_flow": true } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 84575906010..444ff0b5c21 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "step": { "init": { "title": "Configure Tuya Options", diff --git a/requirements_all.txt b/requirements_all.txt index bd7e3474308..e9daee8f38f 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 b15a2d7afce..dc4210dd8ff 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 972903e53e6..b34daaa6d17 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 e9daee8f38f..595cebfe29a 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 645131b60b5..48a662e3a81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -22,10 +22,10 @@ from async_timeout import timeout import voluptuous as vol from homeassistant import exceptions -import homeassistant.components.device_automation as device_automation +from homeassistant.components import device_automation, scene from homeassistant.components.logger import LOGSEVERITY -import homeassistant.components.scene as scene from homeassistant.const import ( + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_ALIAS, CONF_CHOOSE, @@ -44,6 +44,7 @@ from homeassistant.const import ( CONF_REPEAT, CONF_SCENE, CONF_SEQUENCE, + CONF_TARGET, CONF_TIMEOUT, CONF_UNTIL, CONF_VARIABLES, @@ -60,13 +61,9 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.helpers import condition, config_validation as cv, template +from homeassistant.helpers import condition, config_validation as cv, service, template from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables -from homeassistant.helpers.service import ( - CONF_SERVICE_DATA, - async_prepare_call_from_config, -) from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -429,13 +426,13 @@ class _ScriptRun: self._script.last_action = self._action.get(CONF_ALIAS, "call service") self._log("Executing step %s", self._script.last_action) - domain, service, service_data = async_prepare_call_from_config( + domain, service_name, service_data = service.async_prepare_call_from_config( self._hass, self._action, self._variables ) running_script = ( domain == "automation" - and service == "trigger" + and service_name == "trigger" or domain in ("python_script", "script") ) # If this might start a script then disable the call timeout. @@ -448,7 +445,7 @@ class _ScriptRun: service_task = self._hass.async_create_task( self._hass.services.async_call( domain, - service, + service_name, service_data, blocking=True, context=self._context, @@ -755,6 +752,23 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[Dict[str, Any], MappingProxyType] +def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None: + """Extract referenced IDs.""" + if not data: + return + + item_ids = data.get(key) + + if item_ids is None or isinstance(item_ids, template.Template): + return + + if isinstance(item_ids, str): + item_ids = [item_ids] + + for item_id in item_ids: + found.add(item_id) + + class Script: """Representation of a script.""" @@ -889,7 +903,16 @@ class Script: for step in self.sequence: action = cv.determine_script_action(step) - if action == cv.SCRIPT_ACTION_CHECK_CONDITION: + if action == cv.SCRIPT_ACTION_CALL_SERVICE: + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) + + elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_devices(step) elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: @@ -910,20 +933,13 @@ class Script: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: - data = step.get(CONF_SERVICE_DATA) - if not data: - continue - - entity_ids = data.get(ATTR_ENTITY_ID) - - if entity_ids is None or isinstance(entity_ids, template.Template): - continue - - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - for entity_id in entity_ids: - referenced.add(entity_id) + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_entities(step) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 95667d9a690..5f258fc28b7 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1254,3 +1254,6 @@ async def test_blueprint_automation(hass, calls): hass.bus.async_fire("blueprint_event") await hass.async_block_till_done() assert len(calls) == 1 + assert automation.entities_in_automation(hass, "automation.automation_0") == [ + "light.kitchen" + ] diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index c4f39127d93..bb08414b6e8 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8f9e3cec36c..92666335f28 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1338,6 +1338,18 @@ async def test_referenced_entities(hass): "service": "test.script", "data": {"entity_id": "{{ 'light.service_template' }}"}, }, + { + "service": "test.script", + "entity_id": "light.direct_entity_referenced", + }, + { + "service": "test.script", + "target": {"entity_id": "light.entity_in_target"}, + }, + { + "service": "test.script", + "data_template": {"entity_id": "light.entity_in_data_template"}, + }, { "condition": "state", "entity_id": "sensor.condition", @@ -1357,6 +1369,9 @@ async def test_referenced_entities(hass): "light.service_list", "sensor.condition", "scene.hello", + "light.direct_entity_referenced", + "light.entity_in_target", + "light.entity_in_data_template", } # Test we cache results. assert script_obj.referenced_entities is script_obj.referenced_entities @@ -1374,12 +1389,36 @@ async def test_referenced_devices(hass): "device_id": "condition-dev-id", "domain": "switch", }, + { + "service": "test.script", + "data": {"device_id": "data-string-id"}, + }, + { + "service": "test.script", + "data_template": {"device_id": "data-template-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": "target-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": ["target-list-id-1", "target-list-id-2"]}, + }, ] ), "Test Name", "test_domain", ) - assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"} + assert script_obj.referenced_devices == { + "script-dev-id", + "condition-dev-id", + "data-string-id", + "data-template-string-id", + "target-string-id", + "target-list-id-1", + "target-list-id-2", + } # Test we cache results. assert script_obj.referenced_devices is script_obj.referenced_devices diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index eff8b52db16..ab067b004ac 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -9,3 +9,4 @@ trigger: event_type: !input trigger_event action: service: !input service_to_call + entity_id: light.kitchen From 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 ccd5da49547..21b2df308d3 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ - "pyatv==0.7.3" + "pyatv==0.7.5" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 595cebfe29a..849caa4a818 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 dc4210dd8ff..0f0c365f031 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 45c493ad864..9580a7ee50d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.57" + "aiohomekit==0.2.60" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 849caa4a818..62baa006c90 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 0f0c365f031..02ecc56b6a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 67b7508eb94..168ae85b228 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -51,16 +51,15 @@ async def test_hue_bridge_setup(hass): ] for button in ("button1", "button2", "button3", "button4"): - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": button, - "subtype": subtype, - } - ) + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": "single_press", + } + ) triggers = await async_get_device_automations(hass, "trigger", device.id) assert_lists_same(triggers, expected) From 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 8edb1da8f68..59277a394b3 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -194,6 +194,8 @@ class ZHAGroup(LogMixin): """Return entity ids from the entity domain for this group.""" domain_entity_ids: List[str] = [] for member in self.members: + if member.device.is_coordinator: + continue entities = async_entries_for_device( self._zha_gateway.ha_entity_registry, member.device.device_id, From 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 71c8005cbc5..e046a131a6b 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -22,8 +22,9 @@ HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" HOMEKIT_IGNORE = [ - # eufy Indoor Cam 2K Pan & Tilt + # eufy Indoor Cam 2K and 2K Pan & Tilt # https://github.com/home-assistant/core/issues/42307 + "T8400", "T8410", # Hive Hub - vendor does not give user a pairing code "HHKBridge1,1", From 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 41d4fc40e5f..f046f84f94d 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -2,7 +2,7 @@ "domain": "arlo", "name": "Arlo", "documentation": "https://www.home-assistant.io/integrations/arlo", - "requirements": ["pyarlo==0.2.3"], + "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 62baa006c90..2bf1f3b3ee8 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 02ecc56b6a4..a09f961626a 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 9a552502afa..8dea1f5e013 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 c5358988bac..46f68baeb15 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 4ba465a3146..d479efdf95f 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 99cf4be4fff..1dda4ea6df7 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 75c84a5337e..46756b18cb8 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 6031af445a6..967b38cdb82 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 ba4810c3f96..a24c1dbe265 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 31e2791c9f4..b98c6c8e9cd 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 86b06148977..fe461654ecd 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 8bb0486cd24..4baa6aaf047 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 2bf1f3b3ee8..2f7e85ca0bc 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 fb3446fb652..32d33f19e80 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 7f0f920b843..49c10354c51 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 bd35383e981..dd2e29616c7 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 f048eaa3b47..51d9ec20472 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 b76995fe39f..e8b6fcfd2c3 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 00000000000..0f1719b388d --- /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 f378a1fc2a1..16d3f8ba0ac 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 00000000000..5444cd4643d --- /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 ea1db54855b..8d52b7f8d9f 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -5,6 +5,7 @@ from ipaddress import ip_address import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -122,7 +123,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) - if entry.data[CONF_MODEL] == model + if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model ] name = model diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 24f888ea6ef..b9dceec7477 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.axis.const import ( DEFAULT_STREAM_PROFILE, DOMAIN as AXIS_DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -31,6 +31,8 @@ from tests.common import MockConfigEntry async def test_flow_manual_configuration(hass): """Test that config flow works.""" + MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} ) From 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 be967a76fea..6d759ccaf48 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -114,7 +114,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP - if self._device.xy is not None: + if self._device.xy is not None or ( + self._device.hue is not None and self._device.sat is not None + ): self._features |= SUPPORT_COLOR if self._device.effect is not None: @@ -141,8 +143,10 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def hs_color(self): """Return the hs color value.""" - if self._device.colormode in ("xy", "hs") and self._device.xy: - return color_util.color_xy_to_hs(*self._device.xy) + if self._device.colormode in ("xy", "hs"): + if self._device.xy: + return color_util.color_xy_to_hs(*self._device.xy) + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) return None @property @@ -163,7 +167,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + if self._device.xy is not None: + data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + else: + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) if ATTR_BRIGHTNESS in kwargs: data["bri"] = kwargs[ATTR_BRIGHTNESS] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index b971de28d43..18a135a5e05 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -353,3 +353,55 @@ async def test_configuration_tool(hass): await setup_deconz_integration(hass, get_state_response=data) assert len(hass.states.async_all()) == 0 + + +async def test_lidl_christmas_light(hass): + """Test that lights or groups entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = { + "0": { + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + xmas_light_device = gateway.api.lights["0"] + + assert len(hass.states.async_all()) == 1 + + with patch.object(xmas_light_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.xmas_light", + ATTR_HS_COLOR: (20, 30), + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", + "/lights/0/state", + json={"on": True, "hue": 3640, "sat": 76}, + ) + + assert hass.states.get("light.xmas_light") From 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 47bfd031c8e..1d758bc4398 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 cc0e1ef9712..95a51741222 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 e1b95b37e03..9c33637baa8 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 40cf1ccc0b9..8e638205417 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 2ae53598eb4..5d8abfc9f56 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 fa5aa3de317..f1ef5ef665f 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 2aba1db84c3..95400de23b4 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 5ac31e130f9..98407155f17 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 7a80a1b3509..6ebb22b565a 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 fa5dc98c8fa..44f49239642 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 bb9215f0393..591d8c2feaa 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 56c8b5f718a..c67faa25f0b 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 00000000000..e9cd136720f --- /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 0314afa1f53..ef392a5a668 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 00000000000..a55d37ed588 --- /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 45461329fc2..7daa90fad2c 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 0b323a55dc9..77ddb25fb6e 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 baf9ce33cdd..ea99bbf0167 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 21754867038..8ba642263a4 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 5a8ebbeea05..9c2be6fd04b 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 ed650faa027..76c420a584c 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 d62513ec255..f9c19c6be38 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 0cce690257d..e217787ba19 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 d650d6596f7..0c54aecdd88 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 47b39d574b5..5461f207375 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 0e2e7eeb9cc..9f00493666a 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 00000000000..e03867478c4 --- /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 b33812fa24d..725ce07a1b6 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 4a43bae51b2..4a00952aaa5 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 96a09567650..7880adf5fff 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 db021f4afcf..ceb6c92004c 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 00000000000..ce8a9287272 --- /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 188c4a2a6de..7fe3a6078c3 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 99a658c7a0e..0bf8113ccaa 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 2f61dbc37e0..48d278ac354 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 4a071063d47..c2bf0536ccf 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 4a071063d47..c2bf0536ccf 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 83724bb7d05..219446a038d 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 3ad6eaad7ea..e9b298e14ef 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 3cd69cf5ddf..7eacd09c9d9 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 b3b468ff3ec..2d9b2f9e9c5 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 98180e11248..250400b6e22 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 00000000000..d252c078a2c --- /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 0f1e0b522e1..8150e309b20 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 286cd58dd89..47e36006adb 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 d5f64d59f9f..0d8cc552dd2 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 5ce246347c5..ce945ba68d2 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 0c5c7760566..9b067d35f89 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 b2fce54d6b1..91def9afb9d 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 00000000000..7a4274b008c --- /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 462b746816a..eb27f259531 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 f573f0645bc..8eabbbb08cc 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 00000000000..325e9f2e6a0 --- /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 45e45ab85fb..cbd43a04cfd 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 00000000000..20222bd0bce --- /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 00000000000..8c1cb919d11 --- /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 3a3a13b91ce..3e8e4cce2b8 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 2318a5eba05..806b5aebc62 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 eccf48139bd..1f312c187cf 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 fe440913178..36962100519 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 00000000000..7a4274b008c --- /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 19ddbf6057c..64aaf6cbf4a 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 7d765f80f8d..dc21c371e60 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 00000000000..4c984a55690 --- /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 ef31de296c4..c5a4032636c 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 9a0e75c24fa..38375801547 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 00000000000..a04f550a71a --- /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 25623dc04a8..67f59434d46 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 025d58f5197..f53975ecf00 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 7a8b164e32b..d974101d0a1 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 b177591d4e6..72d275069c9 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 f1e1a2f9fe3..4348cecf5c3 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 4e857279e97..a36fd1ca13a 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 ccc0fc7c477..d73c17a62cd 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 b47060ac87c..14f1703524c 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 809cbc360ae..0672c9af33f 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 46558f2e89a..daf07ea4455 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 afab5e616ef..e39ecffa8a7 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 f2766f5a2c0..093b7775829 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 c0a3dae8aae..e92d51b2058 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 4a071063d47..7953cf5625c 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 5edf0b18dd1..a856ef0aeed 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 4285ee45c87..85b6e617963 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 c408b4bec33..6433111fe81 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 6010da17b7b..4553589e36d 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 cbf9bfb6bd4..c4ea835d86c 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 9a5c19acf10..411d3a8610b 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 6e0b2481c8c..e56597a400d 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 3d0630027a8..e9890abba2f 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 65283f66f49..dd40927a7c7 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 51ea84f7c2f..5956937ac2f 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 00000000000..d252c078a2c --- /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 a5648a9138e..626909b9b22 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 99cd4dba4e5..1102078fc62 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 335e097e998..c962097e6ad 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 00000000000..bdafd86c9a0 --- /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 4a071063d47..0918f2cca19 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 7880adf5fff..e67de509456 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 8493f660318..15e61d23627 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 196be985b6a..e3b78cd8e42 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 54f841b2245..75c24278a8c 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 38544eb2cec..4ccc36bcc2a 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 88cfc4a797f..a37c510a366 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 28b7a920e93..592ccd85589 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 43b4c5d49b0..24a12833313 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 ce8a9287272..db0e0c2a137 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 0210147a489..fffb248573d 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 f3da93ad961..0719e226cd6 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 dcc4132df7f..9745f897e05 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 4a071063d47..7953cf5625c 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 e8abc7c8656..cde0a2ad9c7 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 bed3f76be0b..941089ee0cb 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 e7e5968a6d9..bfafaa77b42 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 189f0c1b2f6..e4aaaa39138 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 e4cf0e97879..a92f8d4dbd6 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 c311dbf9eff..c3d4131d995 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 d891eea20a8..908cf287eeb 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 e25dcb40d42..9ef1c325d1e 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 38f054ae4c4..d0c1a3ca188 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 ae7ec8fa5e9..566746538c0 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 3735d12e616..08871c3108e 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 354870a0d51..7a0a8e1a1fd 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 00000000000..94b13c6bc7d --- /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 c30760a9a8f..9f7a5918551 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 dce6f5934d1..b8259aca07f 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 00000000000..7a4274b008c --- /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 83caf877b92..1fe7083ecfd 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 849cb1588f7..313c9057da0 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 a1340f587c9..a800e4d57c6 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 cd1c9b7978e..65edf2dbe31 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 1424e35f359..e25888655a9 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 76d48720110..c887fc385e6 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 ae9d75de54f..324999c7124 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" +DEVICE_INITIALIZED = "yeelight_{}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 @@ -181,8 +181,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" async def _initialize(host: str, capabilities: Optional[dict] = None) -> None: - device = await _async_setup_device(hass, host, entry, capabilities) + async_dispatcher_connect( + hass, + DEVICE_INITIALIZED.format(host), + _load_platforms, + ) + + device = await _async_get_device(hass, host, entry, capabilities) hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device + + await device.async_setup() + + async def _load_platforms(): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -249,28 +260,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def _async_setup_device( - hass: HomeAssistant, - host: str, - entry: ConfigEntry, - capabilities: Optional[dict], -) -> None: - # Get model from config and capabilities - model = entry.options.get(CONF_MODEL) - if not model and capabilities is not None: - model = capabilities.get("model") - - # Set up device - bulb = Bulb(host, model=model or None) - if capabilities is None: - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) - - device = YeelightDevice(hass, host, entry.options, bulb, capabilities) - await hass.async_add_executor_job(device.update) - await device.async_setup() - return device - - @callback def _async_unique_name(capabilities: dict) -> str: """Generate name from capabilities.""" @@ -374,6 +363,7 @@ class YeelightDevice: self._device_type = None self._available = False self._remove_time_tracker = None + self._initialized = False self._name = host # Default name is host if capabilities: @@ -495,6 +485,8 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True + if not self._initialized: + self._initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -522,6 +514,11 @@ class YeelightDevice: ex, ) + def _initialize_device(self): + self._get_capabilities() + self._initialized = True + dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + def update(self): """Update device properties and send data updated signal.""" self._update_properties() @@ -584,3 +581,22 @@ class YeelightEntity(Entity): def update(self) -> None: """Update the entity.""" self._device.update() + + +async def _async_get_device( + hass: HomeAssistant, + host: str, + entry: ConfigEntry, + capabilities: Optional[dict], +) -> YeelightDevice: + # Get model from config and capabilities + model = entry.options.get(CONF_MODEL) + if not model and capabilities is not None: + model = capabilities.get("model") + + # Set up device + bulb = Bulb(host, model=model or None) + if capabilities is None: + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + + return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 9f811586a77..5405b69490b 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -55,7 +55,8 @@ PROPERTIES = { "current_brightness": "30", } -ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight" +ENTITY_BINARY_SENSOR_TEMPLATE = "binary_sensor.{}_nightlight" +ENTITY_BINARY_SENSOR = ENTITY_BINARY_SENSOR_TEMPLATE.format(UNIQUE_NAME) ENTITY_LIGHT = f"light.{UNIQUE_NAME}" ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight" ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index d9c23cfa1a7..882f9944ca1 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,21 +1,27 @@ """Test Yeelight.""" +from unittest.mock import MagicMock + from yeelight import BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, + DATA_CONFIG_ENTRIES, + DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from . import ( + CAPABILITIES, CONFIG_ENTRY_DATA, ENTITY_AMBILIGHT, ENTITY_BINARY_SENSOR, + ENTITY_BINARY_SENSOR_TEMPLATE, ENTITY_LIGHT, ENTITY_NIGHTLIGHT, ID, @@ -115,6 +121,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -132,3 +139,40 @@ async def test_unique_ids_entry(hass: HomeAssistant): assert ( er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight" ) + + +async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): + """Test Yeelight off while adding to ha, for example on HA start.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + **CONFIG_ENTRY_DATA, + CONF_HOST: IP_ADDRESS, + }, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( + IP_ADDRESS.replace(".", "_") + ) + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is None + + type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) + type(mocked_bulb).get_properties = MagicMock(None) + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.async_block_till_done() + + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is not None From 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 ef1e68e2d0a..ea62de71e8d 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -145,12 +145,10 @@ class TransmissionTorrentsSensor(TransmissionSensor): @property def device_state_attributes(self): """Return the state attributes, if any.""" - limit = self._tm_client.config_entry.options[CONF_LIMIT] - order = self._tm_client.config_entry.options[CONF_ORDER] - torrents = self._tm_client.api.torrents[0:limit] info = _torrents_info( - torrents, - order=order, + torrents=self._tm_client.api.torrents, + order=self._tm_client.config_entry.options[CONF_ORDER], + limit=self._tm_client.config_entry.options[CONF_LIMIT], statuses=self.SUBTYPE_MODES[self._sub_type], ) return { @@ -173,11 +171,11 @@ def _filter_torrents(torrents, statuses=None): ] -def _torrents_info(torrents, order, statuses=None): +def _torrents_info(torrents, order, limit, statuses=None): infos = {} torrents = _filter_torrents(torrents, statuses) torrents = SUPPORTED_ORDER_MODES[order](torrents) - for torrent in _filter_torrents(torrents, statuses): + for torrent in torrents[:limit]: info = infos[torrent.name] = { "added_date": torrent.addedDate, "percent_done": f"{torrent.percentDone * 100:.2f}", From 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 3f65ed2ba46..03bf2761857 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.38.0"], + "requirements": ["hass-nabucasa==0.39.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1818385c0e2..5e4bb07394c 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 2f7e85ca0bc..57dcee73b7e 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 a09f961626a..262b0e816e3 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 524b04293ee..f0230aba1b7 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -1,5 +1,6 @@ """Import logic for blueprint.""" from dataclasses import dataclass +import html import re from typing import Optional @@ -110,7 +111,7 @@ def _extract_blueprint_from_community_topic( block_content = block_content.strip() try: - data = yaml.parse_yaml(block_content) + data = yaml.parse_yaml(html.unescape(block_content)) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index bb8903459c9..8e674e3a9de 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -16,6 +16,70 @@ def community_post(): return load_fixture("blueprint/community_post.json") +COMMUNITY_POST_INPUTS = { + "remote": { + "name": "Remote", + "description": "IKEA remote to use", + "selector": { + "device": { + "integration": "zha", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + } + }, + }, + "light": { + "name": "Light(s)", + "description": "The light(s) to control", + "selector": {"target": {"entity": {"domain": "light"}}}, + }, + "force_brightness": { + "name": "Force turn on brightness", + "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n', + "default": False, + "selector": {"boolean": {}}, + }, + "brightness": { + "name": "Brightness", + "description": "Brightness of the light(s) when turning on", + "default": 50, + "selector": { + "number": { + "min": 0.0, + "max": 100.0, + "mode": "slider", + "step": 1.0, + "unit_of_measurement": "%", + } + }, + }, + "button_left_short": { + "name": "Left button - short press", + "description": "Action to run on short left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_left_long": { + "name": "Left button - long press", + "description": "Action to run on long left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_short": { + "name": "Right button - short press", + "description": "Action to run on short right button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_long": { + "name": "Right button - long press", + "description": "Action to run on long right button press", + "default": [], + "selector": {"action": {}}, + }, +} + + def test_get_community_post_import_url(): """Test variations of generating import forum url.""" assert ( @@ -57,10 +121,7 @@ def test_extract_blueprint_from_community_topic(community_post): ) assert imported_blueprint is not None assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS def test_extract_blueprint_from_community_topic_invalid_yaml(): @@ -103,11 +164,11 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit ) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } - assert imported_blueprint.suggested_filename == "balloob/test-topic" + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS + assert ( + imported_blueprint.suggested_filename + == "frenck/zha-ikea-five-button-remote-for-lights" + ) assert ( imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json index 5b9a3dcb9c7..121d53ad94e 100644 --- a/tests/fixtures/blueprint/community_post.json +++ b/tests/fixtures/blueprint/community_post.json @@ -2,39 +2,58 @@ "post_stream": { "posts": [ { - "id": 1144853, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:12.688Z", - "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e", + "id": 1216212, + "name": "Franck Nijhof", + "username": "frenck", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "created_at": "2020-12-10T09:20:58.974Z", + "cooked": "\u003cp\u003eThis is a blueprint for the IKEA five-button remotes (the round ones), specifically for use with ZHA.\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg\" data-download-href=\"/uploads/short-url/8SdGCUtkzOTNpMjggpBvSFs4WQ.jpeg?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg\" alt=\"image\" data-base62-sha1=\"8SdGCUtkzOTNpMjggpBvSFs4WQ\" width=\"500\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_750x750.jpeg 1.5x, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_1000x1000.jpeg 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e1400×1400 150 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eIt was specially created for use with (any) light(s). As the basic light controls are already mapped in this blueprint.\u003c/p\u003e\n\u003cp\u003eThe middle “on” button, toggle the lights on/off to the last set brightness (unless the force brightness is toggled on in the blueprint). Dim up/down buttons will change the brightness smoothly and can be pressed and hold until the brightness is satisfactory.\u003c/p\u003e\n\u003cp\u003eThe “left” and “right” buttons can be assigned to a short and long button press action. This allows you to assign, e.g., a scene or anything else.\u003c/p\u003e\n\u003cp\u003eThis is what the Blueprint looks like from the UI:\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png\" data-download-href=\"/uploads/short-url/mf5vhlKYe6yeuFayUzlCTBfveKf.png?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png\" alt=\"image\" data-base62-sha1=\"mf5vhlKYe6yeuFayUzlCTBfveKf\" width=\"610\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png, https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_915x750.png 1.5x, https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e975×799 64.1 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eBlueprint, which you can import by using this forum topic URL:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003eblueprint:\n name: ZHA - IKEA five button remote for lights\n description: |\n Control lights with an IKEA five button remote (the round ones).\n\n The middle \"on\" button, toggle the lights on/off to the last set brightness\n (unless the force brightness is toggled on in the blueprint).\n\n Dim up/down buttons will change the brightness smoothly and can be pressed\n and hold until the brightness is satisfactory.\n\n The \"left\" and \"right\" buttons can be assigned to a short and long button\n press action. This allows you to assign, e.g., a scene or anything else.\n\n domain: automation\n input:\n remote:\n name: Remote\n description: IKEA remote to use\n selector:\n device:\n integration: zha\n manufacturer: IKEA of Sweden\n model: TRADFRI remote control\n light:\n name: Light(s)\n description: The light(s) to control\n selector:\n target:\n entity:\n domain: light\n force_brightness:\n name: Force turn on brightness\n description: \u0026gt;\n Force the brightness to the set level below, when the \"on\" button on\n the remote is pushed and lights turn on.\n default: false\n selector:\n boolean:\n brightness:\n name: Brightness\n description: Brightness of the light(s) when turning on\n default: 50\n selector:\n number:\n min: 0\n max: 100\n mode: slider\n step: 1\n unit_of_measurement: \"%\"\n button_left_short:\n name: Left button - short press\n description: Action to run on short left button press\n default: []\n selector:\n action:\n button_left_long:\n name: Left button - long press\n description: Action to run on long left button press\n default: []\n selector:\n action:\n button_right_short:\n name: Right button - short press\n description: Action to run on short right button press\n default: []\n selector:\n action:\n button_right_long:\n name: Right button - long press\n description: Action to run on long right button press\n default: []\n selector:\n action:\n\nmode: restart\nmax_exceeded: silent\n\nvariables:\n force_brightness: !input force_brightness\n\ntrigger:\n - platform: event\n event_type: zha_event\n event_data:\n device_id: !input remote\n\naction:\n - variables:\n command: \"{{ trigger.event.data.command }}\"\n cluster_id: \"{{ trigger.event.data.cluster_id }}\"\n endpoint_id: \"{{ trigger.event.data.endpoint_id }}\"\n args: \"{{ trigger.event.data.args }}\"\n - choose:\n - conditions:\n - \"{{ command == 'toggle' }}\"\n - \"{{ cluster_id == 6 }}\"\n - \"{{ endpoint_id == 1 }}\"\n sequence:\n - choose:\n - conditions: \"{{ force_brightness }}\"\n sequence:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n brightness_pct: !input brightness\n default:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n\n - conditions:\n - \"{{ command == 'step_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'step' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [257, 13, 0] }}\"\n sequence: !input button_left_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3329, 0] }}\"\n sequence: !input button_left_long\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [256, 13, 0] }}\"\n sequence: !input button_right_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3328, 0] }}\"\n sequence: !input button_right_long\n\u003c/code\u003e\u003c/pre\u003e", "post_number": 1, "post_type": 1, - "updated_at": "2020-10-20T08:24:14.189Z", + "updated_at": "2020-12-10T09:22:08.993Z", "reply_count": 0, "reply_to_post_number": null, "quote_count": 0, "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", + "reads": 3, + "readers_count": 2, + "score": 0.6, + "yours": false, + "topic_id": 253804, + "topic_slug": "zha-ikea-five-button-remote-for-lights", + "display_username": "Franck Nijhof", "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_bg_color": null, "primary_group_flair_color": null, - "version": 2, + "version": 1, "can_edit": true, "can_delete": false, "can_recover": false, "can_wiki": true, + "link_counts": [ + { + "url": "https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "internal": false, + "reflection": false, + "title": "9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "clicks": 0 + }, + { + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "internal": false, + "reflection": false, + "title": "0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "clicks": 0 + } + ], "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, + "user_title": null, "actions_summary": [ + { + "id": 2, + "can_act": true + }, { "id": 3, "can_act": true @@ -48,75 +67,7 @@ "can_act": true }, { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144854, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:17.535Z", - "cooked": "", - "post_number": 2, - "post_type": 3, - "updated_at": "2020-10-16T12:20:17.535Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 1, - "reads": 2, - "readers_count": 1, - "score": 5.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, + "id": 6, "can_act": true }, { @@ -127,82 +78,9 @@ "moderator": true, "admin": true, "staff": true, - "user_id": 3, + "user_id": 10250, "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "action_code": "visible.disabled", - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144872, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:27:53.926Z", - "cooked": "\u003cp\u003eTest reply!\u003c/p\u003e", - "post_number": 3, - "post_type": 1, - "updated_at": "2020-10-16T12:27:53.926Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, - "can_act": true - }, - { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, + "trust_level": 4, "deleted_at": null, "user_deleted": false, "edit_reason": null, @@ -211,7 +89,7 @@ "reviewable_id": 0, "reviewable_score_count": 0, "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", + "user_created_at": "2017-08-12T12:46:55.467Z", "user_date_of_birth": null, "user_signature": null, "can_accept_answer": false, @@ -220,36 +98,34 @@ } ], "stream": [ - 1144853, - 1144854, - 1144872 + 1216212 ] }, "timeline_lookup": [ [ 1, - 3 + 0 ] ], "suggested_topics": [ { - "id": 17750, - "title": "Tutorial: Creating your first add-on", - "fancy_title": "Tutorial: Creating your first add-on", - "slug": "tutorial-creating-your-first-add-on", - "posts_count": 26, - "reply_count": 14, - "highest_post_number": 27, - "image_url": null, - "created_at": "2017-05-14T07:51:33.946Z", - "last_posted_at": "2020-07-28T11:29:27.892Z", + "id": 168593, + "title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "fancy_title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "slug": "dwains-dashboard-1-click-install-lovelace-dashboard-for-desktop-tablet-and-mobile-v2-0-0", + "posts_count": 1162, + "reply_count": 785, + "highest_post_number": 1185, + "image_url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg", + "created_at": "2020-02-03T13:15:24.364Z", + "last_posted_at": "2020-12-10T07:57:47.304Z", "bumped": true, - "bumped_at": "2020-07-28T11:29:27.892Z", + "bumped_at": "2020-12-10T07:57:47.304Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 18, - "unread": 7, - "new_posts": 2, + "last_read_post_number": 81, + "unread": 0, + "new_posts": 1109, "pinned": false, "unpinned": null, "visible": true, @@ -258,11 +134,19 @@ "notification_level": 2, "bookmarked": false, "liked": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 296, + "height": 50, + "url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg" + } + ], "tags": [], - "like_count": 9, - "views": 4355, - "category_id": 25, + "like_count": 1214, + "views": 71580, + "category_id": 34, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -270,50 +154,50 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 36674, + "username": "dwains", + "name": "Dwain Scheeren", + "avatar_template": "/user_avatar/community.home-assistant.io/dwains/{size}/100261_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9852, - "username": "JSCSJSCS", + "id": 16514, + "username": "jimpower", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/jscsjscs/{size}/38256_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/jimpower/{size}/66909_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 11494, - "username": "so3n", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/so3n/{size}/46007_2.png" + "id": 1473, + "username": "thundergreen", + "name": "Thundergreen", + "avatar_template": "/user_avatar/community.home-assistant.io/thundergreen/{size}/18379_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9094, - "username": "IoTnerd", - "name": "Balázs Suhajda", - "avatar_template": "/user_avatar/community.home-assistant.io/iotnerd/{size}/33526_2.png" + "id": 64369, + "username": "MRobi", + "name": "Mike", + "avatar_template": "/user_avatar/community.home-assistant.io/mrobi/{size}/113127_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 73134, - "username": "diord", - "name": "", - "avatar_template": "/letter_avatar/diord/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 9646, + "username": "Freshhat", + "name": "Freshhat", + "avatar_template": "/user_avatar/community.home-assistant.io/freshhat/{size}/24797_2.png" } } ] @@ -323,19 +207,19 @@ "title": "Lovelace: Button card", "fancy_title": "Lovelace: Button card", "slug": "lovelace-button-card", - "posts_count": 4608, - "reply_count": 3522, - "highest_post_number": 4691, + "posts_count": 4775, + "reply_count": 3635, + "highest_post_number": 4858, "image_url": null, "created_at": "2018-08-28T00:18:19.312Z", - "last_posted_at": "2020-10-20T07:33:29.523Z", + "last_posted_at": "2020-12-10T04:42:58.851Z", "bumped": true, - "bumped_at": "2020-10-20T07:33:29.523Z", + "bumped_at": "2020-12-10T04:42:58.851Z", "archetype": "regular", "unseen": false, "last_read_post_number": 1938, "unread": 369, - "new_posts": 2384, + "new_posts": 2551, "pinned": false, "unpinned": null, "visible": true, @@ -346,8 +230,8 @@ "liked": false, "thumbnails": null, "tags": [], - "like_count": 1700, - "views": 184752, + "like_count": 1740, + "views": 199965, "category_id": 34, "featured_link": null, "has_accepted_answer": false, @@ -366,20 +250,20 @@ "extras": null, "description": "Frequent Poster", "user": { - "id": 2019, - "username": "iantrich", - "name": "Ian", - "avatar_template": "/user_avatar/community.home-assistant.io/iantrich/{size}/154042_2.png" + "id": 33228, + "username": "jimz011", + "name": "Jim", + "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 33228, - "username": "jimz011", - "name": "Jim", - "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" + "id": 12475, + "username": "Mariusthvdb", + "name": "Marius", + "avatar_template": "/user_avatar/community.home-assistant.io/mariusthvdb/{size}/49008_2.png" } }, { @@ -396,32 +280,32 @@ "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 26227, - "username": "RomRider", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/romrider/{size}/41384_2.png" + "id": 52090, + "username": "parautenbach", + "name": "Pieter Rautenbach", + "avatar_template": "/user_avatar/community.home-assistant.io/parautenbach/{size}/89345_2.png" } } ] }, { - "id": 10564, - "title": "Professional/Commercial Use?", - "fancy_title": "Professional/Commercial Use?", - "slug": "professional-commercial-use", - "posts_count": 54, - "reply_count": 37, - "highest_post_number": 54, + "id": 58639, + "title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "fancy_title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "slug": "echo-devices-alexa-as-media-player-testers-needed", + "posts_count": 4429, + "reply_count": 3009, + "highest_post_number": 4517, "image_url": null, - "created_at": "2017-01-27T05:01:57.453Z", - "last_posted_at": "2020-10-20T07:03:57.895Z", + "created_at": "2018-07-04T03:36:22.187Z", + "last_posted_at": "2020-12-10T04:26:11.298Z", "bumped": true, - "bumped_at": "2020-10-20T07:03:57.895Z", + "bumped_at": "2020-12-10T04:26:11.298Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, + "last_read_post_number": 3219, "unread": 0, - "new_posts": 47, + "new_posts": 1298, "pinned": false, "unpinned": null, "visible": true, @@ -431,104 +315,12 @@ "bookmarked": false, "liked": false, "thumbnails": null, - "tags": [], - "like_count": 21, - "views": 10695, - "category_id": 17, - "featured_link": null, - "has_accepted_answer": false, - "posters": [ - { - "extras": null, - "description": "Original Poster", - "user": { - "id": 4758, - "username": "oobie11", - "name": "Bryan", - "avatar_template": "/user_avatar/community.home-assistant.io/oobie11/{size}/37858_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 18386, - "username": "pitp2", - "name": "", - "avatar_template": "/letter_avatar/pitp2/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 23116, - "username": "jortegamx", - "name": "Jake", - "avatar_template": "/user_avatar/community.home-assistant.io/jortegamx/{size}/45515_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 39038, - "username": "orif73", - "name": "orif73", - "avatar_template": "/letter_avatar/orif73/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": "latest", - "description": "Most Recent Poster", - "user": { - "id": 41040, - "username": "devastator", - "name": "", - "avatar_template": "/letter_avatar/devastator/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - } - ] - }, - { - "id": 219480, - "title": "What the heck is with the 'latest state change' not being kept after restart?", - "fancy_title": "What the heck is with the \u0026lsquo;latest state change\u0026rsquo; not being kept after restart?", - "slug": "what-the-heck-is-with-the-latest-state-change-not-being-kept-after-restart", - "posts_count": 37, - "reply_count": 13, - "highest_post_number": 38, - "image_url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png", - "created_at": "2020-08-18T13:10:09.367Z", - "last_posted_at": "2020-10-20T00:32:07.312Z", - "bumped": true, - "bumped_at": "2020-10-20T00:32:07.312Z", - "archetype": "regular", - "unseen": false, - "last_read_post_number": 8, - "unread": 0, - "new_posts": 30, - "pinned": false, - "unpinned": null, - "visible": true, - "closed": false, - "archived": false, - "notification_level": 2, - "bookmarked": false, - "liked": false, - "thumbnails": [ - { - "max_width": null, - "max_height": null, - "width": 469, - "height": 59, - "url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png" - } + "tags": [ + "alexa" ], - "tags": [], - "like_count": 26, - "views": 1722, - "category_id": 52, + "like_count": 1092, + "views": 179580, + "category_id": 47, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -536,72 +328,72 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3124, - "username": "andriej", + "id": 1084, + "username": "keatontaylor", + "name": "Keatontaylor", + "avatar_template": "/letter_avatar/keatontaylor/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 24884, + "username": "h4nc", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/andriej/{size}/24457_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/h4nc/{size}/68244_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 15052, - "username": "Misiu", + "id": 9191, + "username": "finity", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/misiu/{size}/20752_2.png" + "avatar_template": "/letter_avatar/finity/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 4629, - "username": "lolouk44", - "name": "lolouk44", - "avatar_template": "/user_avatar/community.home-assistant.io/lolouk44/{size}/119845_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 51736, - "username": "hmoffatt", - "name": "Hamish Moffatt", - "avatar_template": "/user_avatar/community.home-assistant.io/hmoffatt/{size}/88700_2.png" + "id": 1269, + "username": "ReneTode", + "name": "", + "avatar_template": "/user_avatar/community.home-assistant.io/renetode/{size}/1533_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 78711, - "username": "Astrosteve", - "name": "Steve", - "avatar_template": "/letter_avatar/astrosteve/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 46136, + "username": "chirad", + "name": "Dinoj", + "avatar_template": "/letter_avatar/chirad/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } } ] }, { - "id": 162594, - "title": "A different take on designing a Lovelace UI", - "fancy_title": "A different take on designing a Lovelace UI", - "slug": "a-different-take-on-designing-a-lovelace-ui", - "posts_count": 641, - "reply_count": 425, - "highest_post_number": 654, + "id": 252336, + "title": "Unhealthy state", + "fancy_title": "Unhealthy state", + "slug": "unhealthy-state", + "posts_count": 89, + "reply_count": 69, + "highest_post_number": 91, "image_url": null, - "created_at": "2020-01-11T23:09:25.207Z", - "last_posted_at": "2020-10-19T23:32:15.555Z", + "created_at": "2020-12-05T20:32:00.864Z", + "last_posted_at": "2020-12-09T22:41:30.212Z", "bumped": true, - "bumped_at": "2020-10-19T23:32:15.555Z", + "bumped_at": "2020-12-09T22:41:30.212Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, - "unread": 32, - "new_posts": 615, + "last_read_post_number": 75, + "unread": 0, + "new_posts": 16, "pinned": false, "unpinned": null, "visible": true, @@ -609,12 +401,12 @@ "archived": false, "notification_level": 2, "bookmarked": false, - "liked": false, + "liked": true, "thumbnails": null, "tags": [], - "like_count": 453, - "views": 68547, - "category_id": 9, + "like_count": 33, + "views": 946, + "category_id": 11, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -622,90 +414,179 @@ "extras": null, "description": "Original Poster", "user": { - "id": 11256, - "username": "Mattias_Persson", - "name": "Mattias Persson", - "avatar_template": "/user_avatar/community.home-assistant.io/mattias_persson/{size}/14773_2.png" + "id": 26121, + "username": "helgemor", + "name": "Helge", + "avatar_template": "/user_avatar/community.home-assistant.io/helgemor/{size}/42574_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 27634, - "username": "Jason_hill", - "name": "Jason Hill", - "avatar_template": "/user_avatar/community.home-assistant.io/jason_hill/{size}/93218_2.png" + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46782, - "username": "Martin_Pejstrup", - "name": "mpejstrup", - "avatar_template": "/user_avatar/community.home-assistant.io/martin_pejstrup/{size}/78412_2.png" + "id": 28146, + "username": "123", + "name": "Taras", + "avatar_template": "/user_avatar/community.home-assistant.io/123/{size}/44349_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46841, - "username": "spudje", - "name": "", - "avatar_template": "/letter_avatar/spudje/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 8361, + "username": "kanga_who", + "name": "Jason", + "avatar_template": "/user_avatar/community.home-assistant.io/kanga_who/{size}/46427_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 20924, - "username": "Diego_Santos", - "name": "Diego Santos", - "avatar_template": "/user_avatar/community.home-assistant.io/diego_santos/{size}/29096_2.png" + "id": 44704, + "username": "joselito1", + "name": "jose litomans", + "avatar_template": "/user_avatar/community.home-assistant.io/joselito1/{size}/75914_2.png" + } + } + ] + }, + { + "id": 130280, + "title": "Home Assistant Cast", + "fancy_title": "Home Assistant Cast", + "slug": "home-assistant-cast", + "posts_count": 282, + "reply_count": 206, + "highest_post_number": 289, + "image_url": null, + "created_at": "2019-08-06T15:59:00.183Z", + "last_posted_at": "2020-12-09T16:48:51.132Z", + "bumped": true, + "bumped_at": "2020-12-09T16:48:51.132Z", + "archetype": "regular", + "unseen": false, + "last_read_post_number": 88, + "unread": 0, + "new_posts": 201, + "pinned": false, + "unpinned": null, + "visible": true, + "closed": false, + "archived": false, + "notification_level": 3, + "bookmarked": false, + "liked": false, + "thumbnails": null, + "tags": [], + "like_count": 94, + "views": 29308, + "category_id": 30, + "featured_link": null, + "has_accepted_answer": false, + "posters": [ + { + "extras": null, + "description": "Original Poster", + "user": { + "id": -1, + "username": "system", + "name": "system", + "avatar_template": "/user_avatar/community.home-assistant.io/system/{size}/13_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 11649, + "username": "DavidFW1960", + "name": "David", + "avatar_template": "/user_avatar/community.home-assistant.io/davidfw1960/{size}/66886_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 26084, + "username": "Yoinkz", + "name": "", + "avatar_template": "/letter_avatar/yoinkz/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" + } + }, + { + "extras": "latest", + "description": "Most Recent Poster", + "user": { + "id": 45396, + "username": "Wetzel402", + "name": "Cody", + "avatar_template": "/user_avatar/community.home-assistant.io/wetzel402/{size}/76694_2.png" } } ] } ], - "tags": [], - "id": 236133, - "title": "Test Topic", - "fancy_title": "Test Topic", - "posts_count": 3, - "created_at": "2020-10-16T12:20:12.580Z", - "views": 13, + "tags": [ + "blueprint", + "zha" + ], + "id": 253804, + "title": "ZHA - IKEA five button remote for lights", + "fancy_title": "ZHA - IKEA five button remote for lights", + "posts_count": 1, + "created_at": "2020-12-10T09:20:58.681Z", + "views": 4, "reply_count": 0, "like_count": 0, - "last_posted_at": "2020-10-16T12:27:53.926Z", - "visible": false, + "last_posted_at": "2020-12-10T09:20:58.974Z", + "visible": true, "closed": false, "archived": false, "has_summary": false, "archetype": "regular", - "slug": "test-topic", - "category_id": 1, - "word_count": 37, + "slug": "zha-ikea-five-button-remote-for-lights", + "category_id": 53, + "word_count": 633, "deleted_at": null, - "user_id": 3, + "user_id": 10250, "featured_link": null, "pinned_globally": false, "pinned_at": null, "pinned_until": null, - "image_url": null, + "image_url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", "draft": null, - "draft_key": "topic_236133", - "draft_sequence": 8, - "posted": true, + "draft_key": "topic_253804", + "draft_sequence": 0, + "posted": false, "unpinned": null, "pinned": false, "current_post_number": 1, - "highest_post_number": 3, - "last_read_post_number": 3, - "last_read_post_id": 1144872, + "highest_post_number": 1, + "last_read_post_number": 1, + "last_read_post_id": 1216212, "deleted_by": null, "has_deleted": false, "actions_summary": [ @@ -732,16 +613,24 @@ "bookmarked": false, "topic_timer": null, "private_topic_timer": null, - "message_bus_last_id": 5, + "message_bus_last_id": 4, "participant_count": 1, "show_read_indicator": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 1400, + "height": 1400, + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg" + } + ], "can_vote": false, "vote_count": null, "user_voted": false, "details": { - "notification_level": 3, - "notifications_reason_id": 1, + "notification_level": 1, + "notifications_reason_id": null, "can_move_posts": true, "can_edit": true, "can_delete": true, @@ -756,11 +645,11 @@ "can_remove_self_id": 3, "participants": [ { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "post_count": 3, + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "post_count": 1, "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_color": null, @@ -768,16 +657,16 @@ } ], "created_by": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" }, "last_poster": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" } } } From 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 f0230aba1b7..217851df980 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -108,10 +108,10 @@ def _extract_blueprint_from_community_topic( if block_syntax not in ("auto", "yaml"): continue - block_content = block_content.strip() + block_content = html.unescape(block_content.strip()) try: - data = yaml.parse_yaml(html.unescape(block_content)) + data = yaml.parse_yaml(block_content) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 8e674e3a9de..382363aa560 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -173,6 +173,7 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" ) + assert "gt;" not in imported_blueprint.raw_data @pytest.mark.parametrize( From 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 65fe745e51d..cb52afd4b65 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201204.0"], + "requirements": ["home-assistant-frontend==20201210.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5e4bb07394c..7e69ae0c9e1 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 57dcee73b7e..c8b5ded4cca 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 262b0e816e3..d5a21ee3a1f 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 861e483adb8..096d2c6e24d 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 d8305d10553..ce85d07d086 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 f1821c9e480..7a36348b72c 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 c8b5ded4cca..65bd29256ab 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 d5a21ee3a1f..8497227b164 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 6ba045d60a6..04545a1a422 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 709b9a0ff22..6fcc369182d 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 a4435ccfecf..30b82c65c85 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -397,7 +397,12 @@ class UniFiController: await self.api.login() self.api.start_websocket() - except (asyncio.TimeoutError, aiounifi.AiounifiException): + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.AiounifiException, + ): self.hass.loop.call_later(RETRY_TIMER, self.reconnect) @callback @@ -464,7 +469,12 @@ async def get_controller( LOGGER.warning("Connected to UniFi at %s but not registered.", host) raise AuthenticationRequired from err - except (asyncio.TimeoutError, aiounifi.RequestError) as err: + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.RequestError, + ) as err: LOGGER.error("Error connecting to the UniFi controller at %s", host) raise CannotConnect from err diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 48c080f82f7..94b1c90f4f3 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==25"], + "requirements": ["aiounifi==26"], "codeowners": ["@Kane610"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 65bd29256ab..79f1472043d 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 8497227b164..9604d0579af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 83732601cd6..8d5cb85bf9f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -295,6 +295,22 @@ async def test_get_controller_login_failed(hass): await get_controller(hass, **CONTROLLER_DATA) +async def test_get_controller_controller_bad_gateway(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.BadGateway + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + +async def test_get_controller_controller_service_unavailable(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.ServiceUnavailable + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( From 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 35d0e1fb42e..edf383a6819 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 339b41a9ddc..e081423d590 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 82e5cb8b272..e8783f74bd4 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 8b762a82f02..c5835f8e7c7 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 c3a78133246..25fcf0ad0d2 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 4fcfd8f66f2..c064cca599d 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 aa45f8f92b2..4de2ae4fa6d 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 520bced2385..f871ac89c2d 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 676252c3134..9fa51bebf09 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 ca576f83d2a..5452d15aa30 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 14469e415e0..96d5fe720c3 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 2375fb86e5a..393cb2fcf13 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 4796652f57e..00ad2671391 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 bba5605348b..bb10fd52ae7 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 faf987881b9..e6b22da5af0 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 aea1e40b0f9..712f2e0e376 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 4f4380332fd..673eb169b19 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 ffd34cef8c9..1fda8986ef7 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 24c1c6ff3a1..c61c30c922e 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 761f19ef054..76019680110 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 94356ccf778..75f3bb50309 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 58ec51c4b1b..1c96b3de266 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 b954a97e8f9..f6265d1b96b 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 907e2e4a08f..65acc9afc71 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 9f75f38f1bc..5a8666e5a2e 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 0afd66c10aa..8f4ee3b51c4 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -11,8 +11,6 @@ from homeassistant.helpers import config_validation as cv from .const import ( CONF_ALIASES, - CONF_ALLOW_UNLOCK, - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -36,6 +34,9 @@ from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, is _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_UNLOCK = "allow_unlock" +CONF_API_KEY = "api_key" + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, @@ -61,8 +62,6 @@ def _check_report_state(data): GOOGLE_ASSISTANT_SCHEMA = vol.All( - cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.105"), vol.Schema( { vol.Required(CONF_PROJECT_ID): cv.string, @@ -72,13 +71,14 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( vol.Optional( CONF_EXPOSED_DOMAINS, default=DEFAULT_EXPOSED_DOMAINS ): cv.ensure_list, - vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, - vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, # str on purpose, makes sure it is configured correctly. vol.Optional(CONF_SECURE_DEVICES_PIN): str, vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean, vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT, + # deprecated configuration options + vol.Remove(CONF_ALLOW_UNLOCK): cv.boolean, + vol.Remove(CONF_API_KEY): cv.string, }, extra=vol.PREVENT_EXTRA, ), @@ -113,7 +113,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): await google_config.async_sync_entities(agent_user_id) # Register service only if key is provided - if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config: + if CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 47ceabb20e8..d6badf2e7ba 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -30,9 +30,7 @@ CONF_EXPOSE_BY_DEFAULT = "expose_by_default" CONF_EXPOSED_DOMAINS = "exposed_domains" CONF_PROJECT_ID = "project_id" CONF_ALIASES = "aliases" -CONF_API_KEY = "api_key" CONF_ROOM_HINT = "room" -CONF_ALLOW_UNLOCK = "allow_unlock" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" CONF_REPORT_STATE = "report_state" CONF_SERVICE_ACCOUNT = "service_account" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 4bf0df8b933..5cf1cb14379 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -19,7 +19,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from .const import ( - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -135,11 +134,7 @@ class GoogleConfig(AbstractConfig): return True async def _async_request_sync_devices(self, agent_user_id: str): - if CONF_API_KEY in self._config: - await self.async_call_homegraph_api_key( - REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} - ) - elif CONF_SERVICE_ACCOUNT in self._config: + if CONF_SERVICE_ACCOUNT in self._config: await self.async_call_homegraph_api( REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} ) @@ -164,25 +159,6 @@ class GoogleConfig(AbstractConfig): self._access_token = token["access_token"] self._access_token_renew = now + timedelta(seconds=token["expires_in"]) - async def async_call_homegraph_api_key(self, url, data): - """Call a homegraph api with api key authentication.""" - websession = async_get_clientsession(self.hass) - try: - res = await websession.post( - url, params={"key": self._config.get(CONF_API_KEY)}, json=data - ) - _LOGGER.debug( - "Response on %s with data %s was %s", url, data, await res.text() - ) - res.raise_for_status() - return res.status - except ClientResponseError as error: - _LOGGER.error("Request for %s failed: %d", url, error.status) - return error.status - except (asyncio.TimeoutError, ClientError): - _LOGGER.error("Could not contact %s", url) - return HTTP_INTERNAL_SERVER_ERROR - async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authentication.""" session = async_get_clientsession(self.hass) From 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 362149e9fca..bdcc00dc764 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 5cadc20218e..1aca8c84e07 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 c887fc385e6..b267f6967ef 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 9604d0579af..75f8f1cdece 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 00000000000..33bdcacd37d --- /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 00000000000..78262ccc59c --- /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 00000000000..f217d0b168c --- /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 00000000000..ed49519c771 --- /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 00000000000..d76c714ba5e --- /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 00000000000..e94634c6d15 --- /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 00000000000..1ae8e3f9455 --- /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 f7914177f8a..9e0f8ef51d6 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 7d68d78641e..679d9360a82 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 41edcd176da..ba31fa36fd2 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 f1d7d60315e..26bf8386d66 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 ac804794b48..0cdb9318428 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 8f9931c6820..97e3d088af4 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 a4f73279a76..277c044c533 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 1277d8fca89..639f5834922 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 d2af1633f99..0dd8a3d5543 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 c8a9cd6d50f..326990e12c6 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 73b9134415d..c0c8c11c644 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -21,8 +21,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS, @@ -32,7 +34,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure -from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP +from .const import ( + ATTR_FORECAST_PRECIPITATION, + ATTR_MAP, + CONDITIONS_MAP, + CONF_TRACK_HOME, + DOMAIN, + FORECAST_MAP, +) _LOGGER = logging.getLogger(__name__) @@ -221,6 +230,14 @@ class MetWeather(CoordinatorEntity, WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } + if not self._is_metric: + if ATTR_FORECAST_PRECIPITATION in ha_item: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) + ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 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 d8585ad7458..d0b55514a63 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 7730d8b34c4..78764ccf4e7 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 b285cafcfa9..32567e6d134 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 1b0c86468ea..c9a99f3c205 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 79f1472043d..f81fbe44f1e 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 75f8f1cdece..91c387f9d4f 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 96f135848e1..55478f765e2 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 364953d39d6..a85293e93a0 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 6888266716c..011ddd0b6f7 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 ca41ff6758c..e2607c5f362 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 3107b919e5a..cd8da781870 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 783cd16fefe..519c2e42764 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 b267f6967ef..9e1ec415006 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 cec35eeca29..37bd2fed8a6 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 4a018305bcf..69b413ba51a 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 0f0bdcf4a1c..0f119e7c2ee 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 c34255bc62a..f17a2ce2e4c 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -15,6 +15,15 @@ from homeassistant.helpers import aiohttp_client from . import async_get_client_id from .const import DOMAIN, LOGGER # pylint: disable=unused-import +FULL_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CODE): str, + } +) +PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -24,15 +33,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.full_data_schema = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CODE): str, - } - ) - self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str}) - self._code = None self._password = None self._username = None @@ -125,21 +125,19 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle re-auth completion.""" if not user_input: return self.async_show_form( - step_id="reauth_confirm", data_schema=self.password_data_schema + step_id="reauth_confirm", data_schema=PASSWORD_DATA_SCHEMA ) self._password = user_input[CONF_PASSWORD] return await self._async_login_during_step( - step_id="reauth_confirm", form_schema=self.password_data_schema + step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA ) async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" if not user_input: - return self.async_show_form( - step_id="user", data_schema=self.full_data_schema - ) + return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() @@ -149,7 +147,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] return await self._async_login_during_step( - step_id="user", form_schema=self.full_data_schema + step_id="user", form_schema=FULL_DATA_SCHEMA ) @@ -171,7 +169,9 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_CODE, - default=self.config_entry.options.get(CONF_CODE), + description={ + "suggested_value": self.config_entry.options.get(CONF_CODE) + }, ): str } ), From 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 a12754a87f4..fcfb4cf7ff1 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 5d4aa18ee2b..4c88c157f9b 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 78262ccc59c..8eb8b0d9a32 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 00000000000..16a2f8b3f0d --- /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 f217d0b168c..1bf6f0f3bef 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 ed49519c771..fe7298b40cd 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 d76c714ba5e..1a36e5421ec 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 e94634c6d15..45fdd01a643 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 1ae8e3f9455..05151d38be8 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 773a872f33f..c9f96a0e9d7 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -43,6 +43,8 @@ async def async_discover_and_construct( ) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name + _LOGGER.debug("Constructing device: %s::%s", udn, st) + discovery_infos = await Device.async_discover(hass) _LOGGER.debug("Discovered devices: %s", discovery_infos) if not discovery_infos: @@ -53,7 +55,7 @@ async def async_discover_and_construct( # Get the discovery info with specified UDN/ST. filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn] if st: - filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st] + filtered = [di for di in filtered if di[DISCOVERY_ST] == st] if not filtered: _LOGGER.warning( 'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting', @@ -74,6 +76,7 @@ async def async_discover_and_construct( ) _LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name) + _LOGGER.debug("Constructing from discovery_info: %s", discovery_info) location = discovery_info[DISCOVERY_LOCATION] return await Device.async_create_device(hass, location) @@ -104,7 +107,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data) + _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) # Discover and construct. udn = config_entry.data.get(CONFIG_ENTRY_UDN) @@ -123,6 +126,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Ensure entry has a unique_id. if not config_entry.unique_id: + _LOGGER.debug( + "Setting unique_id: %s, for config_entry: %s", + device.unique_id, + config_entry, + ) hass.config_entries.async_update_entry( entry=config_entry, unique_id=device.unique_id, @@ -152,6 +160,8 @@ async def async_unload_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: """Unload a UPnP/IGD device from a config entry.""" + _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) + udn = config_entry.data.get(CONFIG_ENTRY_UDN) if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: del hass.data[DOMAIN][DOMAIN_DEVICES][udn] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 72efc4ffd55..7b20c7709a0 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -154,6 +154,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() # Store discovery. + _LOGGER.debug("New discovery, continuing") name = discovery_info.get("friendlyName", "") discovery = { DISCOVERY_UDN: udn, diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5f29043a1fe..6bc497170ca 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -109,7 +109,7 @@ class Device: def __str__(self) -> str: """Get string representation.""" - return f"IGD Device: {self.name}/{self.udn}" + return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" async def async_get_traffic_data(self) -> Mapping[str, any]: """ From 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 9e1ec415006..01c0a657f31 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 e56d35c1dff..fbaeb502b42 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 bf6716ec966..886b67f8e2a 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 cb52afd4b65..caf309e6718 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201210.0"], + "requirements": ["home-assistant-frontend==20201212.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e69ae0c9e1..180305e8ed8 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 f81fbe44f1e..855cfbc311a 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 91c387f9d4f..7da703b01da 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 0875e094352..0175dfd6586 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -23,7 +23,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def _await_cancel(task): diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 8a13334bc6d..9c26f3552aa 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -32,7 +32,7 @@ from .helpers import async_get_blueprints _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), + cv.deprecated(CONF_HIDE_ENTITY), script.make_script_schema( { # str on purpose diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index c3fd2a6ff00..fd2f08c1488 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -30,7 +30,7 @@ from .coordinator import CanaryDataUpdateCoordinator MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"), + cv.deprecated(CONF_FFMPEG_ARGUMENTS), PLATFORM_SCHEMA.extend( { vol.Optional( diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 3ebb919393a..446890887c1 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -33,10 +33,10 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_EMAIL, invalidation_version="0.119"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.119"), - cv.deprecated(CONF_ZONE, invalidation_version="0.119"), - cv.deprecated(CONF_RECORDS, invalidation_version="0.119"), + cv.deprecated(CONF_EMAIL), + cv.deprecated(CONF_API_KEY), + cv.deprecated(CONF_ZONE), + cv.deprecated(CONF_RECORDS), vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 7b9c1ded673..b4950b8b05b 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -30,7 +30,7 @@ COMPONENT_TYPES = ["climate", "sensor", "switch"] CONFIG_SCHEMA = vol.Schema( vol.All( - cv.deprecated(DOMAIN, invalidation_version="0.113.0"), + cv.deprecated(DOMAIN), { DOMAIN: vol.Schema( { diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 59682178d40..22a97b9e82e 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -22,7 +22,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 7399dd3847d..46442f112b6 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -20,7 +20,7 @@ from .const import ( DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["sensor"] diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index b8e9040f7ce..5aa087c0515 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -80,13 +80,13 @@ SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT # Usage of YAML for configuration of the Hyperion component is deprecated. PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"), + cv.deprecated(CONF_HDMI_PRIORITY), cv.deprecated(CONF_HOST), cv.deprecated(CONF_PORT), - cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"), + cv.deprecated(CONF_DEFAULT_COLOR), cv.deprecated(CONF_NAME), cv.deprecated(CONF_PRIORITY), - cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"), + cv.deprecated(CONF_EFFECT_LIST), PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index f787c028762..637520aa30c 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -11,7 +11,7 @@ from .const import DOMAIN, PLATFORM CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_NAME, invalidation_version="0.110"), + cv.deprecated(CONF_NAME), vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string}), ) }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 73caf023ef6..cced3670cca 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -191,7 +191,7 @@ def embedded_broker_deprecated(value): CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_TLS_VERSION, invalidation_version="0.115"), + cv.deprecated(CONF_TLS_VERSION), vol.Schema( { vol.Optional(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 561f3edf896..88da19f5ab2 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -30,7 +30,7 @@ ATTR_SYSTEM_NAME = "system_name" DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a520772ff77..41c56e38db6 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -74,7 +74,7 @@ SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 739e345a637..af2e0ee946f 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index eecac0281e6..6be02b9ba5e 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -33,7 +33,7 @@ from .const import ( ENTITY_COMPONENTS, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.117") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2430aad43cf..89f5c40b1ff 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -138,7 +138,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( } ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) @callback diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 94795a10c83..c6f420d172a 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -40,7 +40,7 @@ DOMAIN = const.DOMAIN CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(const.CONF_PROFILES, invalidation_version="0.114"), + cv.deprecated(const.CONF_PROFILES), vol.Schema( { vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 81b522308ff..cd3b1bd93ce 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -254,8 +254,10 @@ class ZHADevice(LogMixin): "device_event_type": "device_offline" } } + if hasattr(self._zigpy_device, "device_automation_triggers"): triggers.update(self._zigpy_device.device_automation_triggers) + return triggers @property diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index dea8deec715..0513c5c6e7e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -28,7 +28,6 @@ from typing import ( from urllib.parse import urlparse from uuid import UUID -from pkg_resources import parse_version import voluptuous as vol import voluptuous_serialize @@ -80,7 +79,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, - __version__, ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError @@ -712,7 +710,6 @@ class multi_select: def deprecated( key: str, replacement_key: Optional[str] = None, - invalidation_version: Optional[str] = None, default: Optional[Any] = None, ) -> Callable[[Dict], Dict]: """ @@ -725,8 +722,6 @@ def deprecated( - No warning if only replacement_key provided - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected """ module = inspect.getmodule(inspect.stack()[1][0]) if module is not None: @@ -737,56 +732,24 @@ def deprecated( # https://github.com/home-assistant/core/issues/24982 module_name = __name__ - if replacement_key and invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please replace it with '{replacement_key}'." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) - elif replacement_key: + if replacement_key: warning = ( "The '{key}' option is deprecated," " please replace it with '{replacement_key}'" ) - elif invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please remove it from your configuration." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) else: warning = ( "The '{key}' option is deprecated," " please remove it from your configuration" ) - def check_for_invalid_version() -> None: - """Raise error if current version has reached invalidation.""" - if not invalidation_version: - return - - if parse_version(__version__) >= parse_version(invalidation_version): - raise vol.Invalid( - warning.format( - key=key, - replacement_key=replacement_key, - invalidation_status="became", - invalidation_version=invalidation_version, - ) - ) - def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: - check_for_invalid_version() KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning, key=key, replacement_key=replacement_key, - invalidation_status="will become", - invalidation_version=invalidation_version, ) value = config[key] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 480b2280afe..5d907408b61 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -698,116 +698,6 @@ def test_deprecated_with_replacement_key(caplog, schema): assert test_data == output -def test_deprecated_with_invalidation_version(caplog, schema, version): - """ - Test deprecation behaves correctly with only an invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema without changing any values - - No warning or difference in output if key is not provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated("mars", invalidation_version="9999.99.9"), schema - ) - - message = ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. " - "This option will become invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert message in caplog.text - assert test_data == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"venus": False} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", invalidation_version="0.1.0"), schema - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. This option became " - "invalid in version 0.1.0" - ) - - -def test_deprecated_with_replacement_key_and_invalidation_version( - caplog, schema, version -): - """ - Test deprecation behaves with a replacement key & invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning or difference in output if neither key nor - replacement_key are provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", replacement_key="jupiter", invalidation_version="9999.99.9" - ), - schema, - ) - - warning = ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert warning in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_with_default(caplog, schema): """ Test deprecation behaves correctly with a default value. @@ -894,69 +784,6 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): assert {"jupiter": True} == output -def test_deprecated_with_replacement_key_invalidation_version_default( - caplog, schema, version -): - """ - Test deprecation with a replacement key, invalidation_version & default. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning if neither key nor replacement_key are provided - - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", - replacement_key="jupiter", - invalidation_version="9999.99.9", - default=False, - ), - schema, - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert {"venus": True, "jupiter": False} == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_cant_find_module(): """Test if the current module cannot be inspected.""" with patch("inspect.getmodule", return_value=None): @@ -964,7 +791,6 @@ def test_deprecated_cant_find_module(): cv.deprecated( "mars", replacement_key="jupiter", - invalidation_version="1.0.0", default=False, ) diff --git a/tests/test_config.py b/tests/test_config.py index bfda156f2b7..931b672d01b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1116,7 +1116,7 @@ async def test_component_config_exceptions(hass, caplog): ("non_existing", vol.Schema({"zone": int}), None), ("zone", vol.Schema({}), None), ("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"), - ("openuv", cv.deprecated("openuv", invalidation_version="0.115"), None), + ("openuv", cv.deprecated("openuv"), None), ], ) def test_identify_config_schema(domain, schema, expected): From 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 e012d68a10e..4d6e23022aa 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 04cb13bf5e8..211f35d62ce 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 4ef73de7232..3fe3d33793f 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 926e936a594..bc600f8428c 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 44031e846c3..b6690ed6d28 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 00000000000..e7c0712272c --- /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 c7325f776fa..0d5ded7b828 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 4c88c157f9b..1362c7d483c 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 e2210d0279a..50926e07a11 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 69ab0a3421a..280956c7ddd 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 1848c890573..753ac2a2441 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.6"], + "requirements": ["zeroconf==0.28.7"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 180305e8ed8..7cdec1fd7b8 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 855cfbc311a..3c3d5dbe647 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 7da703b01da..52663d3f893 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 7a36348b72c..ed2460614fc 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 3c3d5dbe647..c9299bab074 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 52663d3f893..0b8b09ef174 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 486e9f1643c..d188dd270ab 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 c9299bab074..b711bb95287 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 0b8b09ef174..a56ab2d42b1 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 775443d8f5f..eefa1fd2ddd 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 00000000000..3d5be90042e --- /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 d9a17545c4b..556e9b4136b 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 067234c7b54..c3abf6ceba3 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 33233840720..752136dac7f 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 7c90b937329..b128be67087 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 34295b718ca..e280388eeca 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 c8341a3ec2c..31085292fbb 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 b711bb95287..45122f548cb 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 a56ab2d42b1..fd03379d222 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 27614c3d49d..8e2dd1fa56d 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 3a94215a7b1..98fbf2d961a 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 7d78491ad14..30b9a2066cd 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 fad88a19b34..0b5af0ae432 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 45122f548cb..c1c78b425ac 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 fd03379d222..c6c65f5226a 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 b834cbc0aab..3c07fbc4e20 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 c1c78b425ac..1fc3ba1d968 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 491d400411c..69e4ce0c016 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 fb36312cf1e..4c534a90ae1 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 49edf8e7d0a..06132e83e88 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 27656c260d3..670f5e66146 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 70d66053209..4fb25c766cc 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 1fc3ba1d968..50469e89516 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 c6c65f5226a..6f7316d8a45 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 4e6b71d59b7..dc8a85ce2aa 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 7ce75b1e3fa..53304c93218 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 50469e89516..d8ba24aa21a 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 6f7316d8a45..8dc7f9e1fee 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 fa4883ae5a2..d105572c182 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 12d928e172a..1d3f767353b 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 05a12bc2284..e071a523321 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 e2b4056cfaa..4dcccc98c05 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 00000000000..b4772e51742 --- /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 00000000000..947bad37e01 --- /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 136af1f4be9..1ea52d4e604 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 e6ab8dbf6a9..b339013a69f 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 d8ba24aa21a..36fab4e430e 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 00000000000..1b6e9caa24c --- /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 00000000000..4eea4abc155 --- /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 00000000000..0ddc466a6f7 --- /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 00000000000..acec471c171 --- /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 00000000000..94acde2d0a3 --- /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 320ca70b810..fbe13abc41e 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 00000000000..0acb1e6a9a6 --- /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 00000000000..b7c7b438804 --- /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 6bb5ae14603..87042b6500e 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 00000000000..94c31d0e156 --- /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 00000000000..aafc2e2b303 --- /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 26bf8386d66..5d1e99d17f4 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 937b6099bd9..385a0eedff2 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 981cb51c83a..d368ac0fb3c 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 00000000000..1ff8ea1b3a9 --- /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 00000000000..6f46000e3e2 --- /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 00000000000..488ad379942 --- /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 00000000000..49fa9545e94 --- /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 00000000000..c8680fcb7e5 --- /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 00000000000..9f763d0d6cc --- /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 00000000000..545a3547ffc --- /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 3f55b19b255..42847d89fda 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 00000000000..484cdaff6ec --- /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 827d8078b55..3b651ce82b0 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 00000000000..f3784f6de87 --- /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 00000000000..d0a70d57752 --- /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 e2e01eb1df7..2282e3584fc 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 00000000000..d25f1975cf7 --- /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 00000000000..5307276a71d --- /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 c7307d119a0..c543f155e4d 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 00000000000..6b9d85fd9fb --- /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 00000000000..1b08426f631 --- /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 0dd8a3d5543..5a4de08033c 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 00000000000..14365f988bd --- /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 00000000000..3010c9e622b --- /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 9a69f0b444c..c96a990433a 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 a8b10eb4501..e479f5e9ac1 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 41b46ae5cb8..ad95609bd9f 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 05a8f96bda7..19f7e265438 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 36fab4e430e..800204a2d04 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 fdbba4212f6..c3f6aa4dea3 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 800204a2d04..e60fcdf5dd3 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 8dc7f9e1fee..da4c547a4f5 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 fbaeb502b42..368eb8b3465 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 c656926ecff..d9d6a16ebf7 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 00000000000..87a6ff39552 --- /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 0d5ded7b828..0dca71a0d8d 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 fe7298b40cd..38055ba972c 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 fc592c9e5c0..a39b6b300d2 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 a4e0adc81a8..0560e6541bd 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 728e54b456f..a831b55606e 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 00000000000..49b528645ea --- /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 696412ac3c7..4542506bec5 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 69450c4c4dc..ea84bf34586 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 e60fcdf5dd3..728dc8ca348 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 da4c547a4f5..1d0a8c6fa59 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 60293612cd3..028f56587a1 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 728dc8ca348..3ae5d51161b 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 1d0a8c6fa59..7f9a9ad69b9 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 0bd7159cf97..32e4902799e 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 a4c6f77fc13..5b298d44ce0 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 3ae5d51161b..50ee26f7fdc 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 7f9a9ad69b9..5389f0993d7 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 88e01f04bc5..40ac452a9da 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 b0f17918cd8..a5d36868863 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 00000000000..b1ed434e211 --- /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 39be96b84d5..4b4aa37640a 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 00000000000..82687d50bf9 --- /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 00000000000..35636c0fd95 --- /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 00000000000..45768a9f127 --- /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 6f398062876..7ba59cf0647 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 6b9d85fd9fb..23fe89c73b4 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 07e72a29609..496c279ba21 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 00000000000..e702c18e89f --- /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 4dfb58ef3b7..49cec207764 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 28672ef409c..3edc1ce2cde 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 2fff760bb70..8ddb6e82eda 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 aa1f0b8814a..195e4c2242e 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 d40a88e3f43..a336ef82363 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 8e2dd1fa56d..e5d67234f6f 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 c839cb75933..2f3e8cf4f1a 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 50ee26f7fdc..eaf21edabfa 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 3a795b60420..8072e06c2e5 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 eaf21edabfa..80f685845ae 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 5389f0993d7..fef2cba80d0 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 40ac452a9da..261c1898ca9 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 75ac015b83b..71c971757c1 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 01c0a657f31..a8459a2cd74 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 9775dc592fd..1d9d3de4f89 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 00000000000..931d7cdb712 --- /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 4d7c4129d81..1698a1d944a 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 f74364bc8bc..449de72b158 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 53948e2b19d..248e455b6da 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 d36e3fa503f..d3ea8a8525c 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 efcbfb8d54c..b083ec1d7df 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 5d71d4889ac..21af0f91d17 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 a6aa19abe26..204adb108a8 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 c41d4e6d93a..c8dcc93500b 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 61b6ad44dfd..333c8a980f0 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 677bed1565b..ce4156244b7 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 80f685845ae..20e3c4c115c 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 fef2cba80d0..27fe91d0eb4 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 31c2cddd09d..6954eb1b7af 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 182ef98e529..00000000000 --- 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 13dd977b7dc..05494d14869 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 aef74e530b1..11ab3289d14 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 9875f3bd918..6d61af0b66f 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 5aa087c0515..e2989cb973b 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 180f266f1af..ca7ed238f0b 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 a2febcca2a5..31a6c49eeb3 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 807a3829e7b..481b7957849 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 5366f6e14d1..4636a9ad59c 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 469c0fa7fbb..de18fd4ed04 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 ce310001b35..1580969b0a5 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 4f5601a7f30..4bd5903253a 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 20e3c4c115c..489b4198bef 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 27fe91d0eb4..a4150cbfcdd 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 3a4ae887aae..7ed3306721c 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 5d1e99d17f4..26bf8386d66 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 c4c4f512d6f..d1277b411e0 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 c8dcc93500b..c41d4e6d93a 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 333c8a980f0..cc633979645 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 7f0213be4ef..165e0bfd98a 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 451e8d6a3c2..7ccec14406a 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 29828bddc17..87e549b9298 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 fca2761f2f3..24a98cbf155 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 d7d45bbd7e3..e5ae80022b8 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 28f2aca4fbb..cb0ccf268f7 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 45b98d7c27c..abc53294bbc 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 a831b55606e..78429dd1fe0 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 49b528645ea..c4abc12d240 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 00000000000..3d7b1b8fc13 --- /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 14170fdd8cd..eca5e91ddeb 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 b7486af50e9..81bb79dc50b 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 d8c6a2c352e..7d6c6222d2e 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 fdf48ebba5d..6c945c81115 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 00000000000..033b4aa7b4d --- /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 00000000000..1a6f60537b3 --- /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 b76dbcaf20b..e68800efb44 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 0e9f6c13e3d..4f75e93faef 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 ee3471725b6..5f863ad7f34 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 489b4198bef..ba1a755176a 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 a4150cbfcdd..992b3d0816c 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 8b3b1949ec3..f745c756898 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 87e549b9298..197864b807c 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 e5ae80022b8..46dc5510b18 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 57bd346c91b..10d39c1fc40 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 f0d1527a0fb..402c143706e 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 8012bdbb02b..4a6c9dbda6c 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 53304c93218..405d66989bb 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 bec87b72ee4..1f17e1f60d2 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 a39b6b300d2..b6ca7881615 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 29f6ade8af4..80d9b50932e 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 8bdeef25118..b617826411d 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 0560e6541bd..5e2b06564dd 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 ba1a755176a..3a048845626 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 0055d323938..d642d3c6e0f 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 3c88914aafd..00f3c2da2cb 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 66303ad96b3..ce07f5ab615 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 0aa390223ca..454fcc976f9 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 a5d36868863..43d6ba21ca5 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 9291e17e865..1dd547b6fcf 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 4d9689ccf5f..7f3855a7b9c 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 b1ed434e211..4c726019424 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 bbfc9f7b311..945f467c4cf 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 b69e8de8f7c..ed0ef205ff0 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 46f68baeb15..52e3f0beb52 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 00000000000..0d6bcb67479 --- /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 e8d6232236b..a225b7f2c47 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 0510da2305e..ff3170ffb98 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 79c90379f18..c73f5205971 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 fda9ef4bb5b..9e74680a951 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 fb9cbe3b7a8..ed003131bf2 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 62e2e1e59c5..9fa766c190b 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 d14dfa6c65a..99d11e5d6c9 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 00000000000..3fc69f85947 --- /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 12f0a2859e8..13f8c6bd800 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 0a2f8461bed..22895399156 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 bc53bc93f7a..5d45710f4a6 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 c41d4e6d93a..426dbbe399c 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 72ce208db3c..0c0aaa5f172 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 989bf9ce137..614465472e6 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 fcbc0361c24..dbec3effa19 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 e4a30539673..30ea15c60c3 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 602b3d47dcf..beddee423a4 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 42847d89fda..400fd8eb933 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 f2450bf6f69..2c8402a7169 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 81a2390cc8b..815b87f2ec0 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 8fc660e8128..c7dfe6d02b2 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 496c279ba21..e3183fb5c59 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 a5f4ff11f09..ff3887c840d 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 fdbf8971238..dfc337fc844 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 6d61af0b66f..2bb9ec241e5 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 7ffa8ed24bf..57f58f05993 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 3a048845626..3d610228601 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 992b3d0816c..7ffaf2f83c3 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 a8bdb2b9c43..a494b96abf1 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 3527a05e5b3..500c243b8c9 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 8ef9413edbf..24a1589db0d 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 3d610228601..fadf7ea3f20 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 7ffaf2f83c3..755a5d6fac5 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 0febf142d02..8ef11335199 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 4e22591d3b0..8a03f13beda 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 56b181aa02a..27bf0414dd3 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 27bf0414dd3..cd394234ccf 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 41c56e38db6..c8697adbcd4 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 5f38c420ff1..265ec01b6db 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 db0df3a073b..bf1725a036a 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 e43c61985d6..ecd1e3c3c4b 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 5ab331df44e..6445b4ad91f 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 fadf7ea3f20..411b7825773 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 755a5d6fac5..e22f5503fc6 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 2ef904f8680..c9de7eea7d4 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 51d9ec20472..85d79b6b331 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 1dd547b6fcf..9c924e7d978 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 00000000000..7ccc7c7360a --- /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 20222bd0bce..50ac384d8ca 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 601af3a68e9..f818135c51f 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 614465472e6..100237c33e6 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 ed7f5affc56..abf14268687 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 ebd284eaa01..568f77d622a 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 ae182c62dc6..ec35a448407 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 0a41f8e2a8f..6e0e78839c1 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 507b48da9db..fec7b82e365 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 584ce708d15..d0c0e9eccc8 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 c42a53b2dae..601ce1efbfe 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 9eeaf6fccca..01c4047af65 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 cc8f9a17827..6e8c09bbd60 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 4582fc5f3b6..44f5c9c56f7 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 bed0d2b8d17..279bc0f686f 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 6dabfdf0447..ba29ff4a8da 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 b3af06ad070..cebfd95591f 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 fa9ed6b467f..cbccc3a462d 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 4f61322b2c6..90058e254dc 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 d4aae9a94fc..185f6024886 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 28597b3859e..6e3d70b0815 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 b45ca4497a4..ed44f1aa728 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 344ea569104..d5a61fd18ae 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 411b7825773..91c5de815b9 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 e22f5503fc6..1e9eb6842ad 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 14756f1183c..3649c954b52 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 6adea1af5af..ffcaedeb5a0 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 91c5de815b9..6ecf92e0626 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 1e9eb6842ad..77351567f3f 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 d9de9b9d558..68f762a54fc 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 6ecf92e0626..6b4b5d676e1 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 f745c756898..58d6a4295e9 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 f1b873d58a8..72300c1621e 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 454fcc976f9..8b779696a70 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 3492518421d..d1c12e657fd 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 d4ba26bd484..ec7ad592f15 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 10d39c1fc40..0600d73d8a1 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 402c143706e..8e208f57cc6 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 405d66989bb..d66c2aae0e4 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 0cd251c737b..a350b9880fc 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 28d73d189b8..e9deabec71b 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 1f17e1f60d2..ca3fbe8be56 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 ed44f1aa728..fc1f0c9a707 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 3649c954b52..92d8d2638b9 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 38f487a98a1..301bd1976e6 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 e1ee75297fd..8db8afa3a6b 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 10eb5dfd1e3..22ce4dba676 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 00000000000..a8d0d7ffbd3 --- /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 9c924e7d978..5a13056e685 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 78db52ade45..a02601759be 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 e2a9646a0aa..0d8a0052406 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 d4f8fc4bcc6..6ea40d0fd00 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 7c3467ca550..63012e23da1 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 c00ee65c278..3f1b7ef816e 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 00000000000..bb1f8e025ca --- /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 05f56150169..92ad0e22663 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 2b96bdfa5b8..2ced7577fdf 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 acec471c171..95312fe7943 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 d0309140903..4706350cdb3 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 32d738d838d..0ed34907b1a 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 cd849b9f933..1c940ea7a35 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 ec5ad6c53ca..f5116110a09 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 e34db6d7262..f915040635f 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 4c07d1a2997..72bd052cc1d 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 4b4aa37640a..5fd61c0bfed 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 159f961c3a6..eebc9bd5fc3 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 68b18568156..809dad5da46 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 1653a11c3ed..f9a5783cd91 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 19a57c3180e..908dfaa448c 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 1d9ede292f6..bbac113eb44 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 3b5744ba1ae..fadb459a3d3 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 4d10424216e..74974604453 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 82687d50bf9..c03615a39ff 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 6f398062876..38215675701 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 e7dc1f6cd27..cd2934170c9 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 80db678f279..d79c03f0179 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 7407782bc3f..27770d690f0 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 7c5e1d87c9d..92ab6c1c8ff 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 6f398062876..94b8d6526d1 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 451720ffb16..7da997f12d6 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 00000000000..21aa58a9e64 --- /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 e705d115c8d..33b7c927520 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 f50a90c8a88..a0bf05cb5ec 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 0db5c5a422e..74c204b9683 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 00000000000..f8dac159488 --- /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 dbec3effa19..9db6c2450ff 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 3b7054ea661..3177ed9d8e8 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 a7ad0fe1d27..8581b04099d 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 2665a5c5a85..7d0bce9ff1f 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 2ebae1083eb..018f3870c58 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 6f398062876..38215675701 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 35232fe04db..239b47e2d3e 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 7ba59cf0647..3bd083e4839 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 cac04acb87a..4b2c14be9d6 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 f55df964f86..accee16a6f5 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 f772a8586d0..2e5bed4b668 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 71dd785b4ff..5dd638a717c 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 e2c38ad5d05..3c240d96131 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 e1402d12e42..8bbc0de94b4 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 6c4932505ba..eb9f73fef99 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 6d934ed0e60..1979a10cb8a 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 a2c942db57f..ad863f7ff79 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 9e6453222a9..9918e38670a 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 1b5c5cb760e..ae15ec058b5 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 c083048e4cd..e3354267630 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 5a1d4f2f185..2294960d6f2 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 dac21a6af74..74d0f831c8b 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 67f059c1cd3..ab05cf649d8 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 0e77c8fbd7a..a609492f428 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 6f398062876..38215675701 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 273daf6ef0a..1252313a438 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 24087b50617..667bf6dbd12 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 47c01a0309f..303321ea94c 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 67ad3cc5e59..09100c355c2 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 cd8edbc3e56..670f57df8ba 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 e3183fb5c59..4cdcdfced79 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 2ae8c2863af..27ba9bb29c7 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 e702c18e89f..2b4c70a0bad 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 ffdd1e0dd58..76bbc705690 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 d9013bea391..c6c872c85e6 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 f2b24b2c553..ddb68ec09fa 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 00000000000..ef455299de6 --- /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 97c9da5794b..151b1dac000 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 6aaa5bcc489..36b0da239a9 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 f945469e26f..6ce529621aa 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 6573b17980e..e506f269d66 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 a69871a1ef6..ebf31967cc7 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 b39d7f27c55..5e0e1b5761a 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 6b4b5d676e1..dac983b1acd 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 77351567f3f..bf0074d3191 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 2a9ae1187b2..ab76d14a69a 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 1658fcec1f5..69b276fc75f 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 77ce53abef7..5df508d0f33 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 c215ec7dae9..27706c3d797 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 138b84f6fda..abf4a9f62e4 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 03606193945..1b0f03b8018 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 88a4a0bdbb2..c39aa7637f8 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 4eea4abc155..26c02fabbb4 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 6a418ccdc93..ae314897e74 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 ea0f1baa067..c0252e045b2 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 43ffe5960c7..98486a28b09 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 bc649dcadf0..50ee7a7a2a9 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 e2be7cf4097..0b99005c382 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 8c72e9d92f8..d64fedecc5f 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 5dfc087c7bb..a2bea167b5e 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 585811f0eb4..63779e7fa94 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 5d831d7aaa2..d3501c496ef 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 00000000000..63c887ff281 --- /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 5a4635e1ed8..af1ceba2c97 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 1ea5c9020e7..fa341509634 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 4119802eb77..216e8d391b6 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 3d95664d0ab..e929ea2f919 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 00000000000..50ccd9f3b63 --- /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 c73f5205971..e411982b58a 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 2f9571b68fa..62e123eb84c 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 14ffab87116..94aa56bb63c 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 c44f51b02e1..301075e0ad4 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 e508f2cb29e..63ceded5654 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 1d88e45b2ca..f2fd30f323e 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 9db6c2450ff..a788c79ff5d 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 daf07ea4455..08d8cb97454 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 739d77c8268..6693c2e5614 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 2e58ddeeddf..7d22dfd96bf 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 d9a216305e8..47334c4aa62 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 4db72851aa3..958eaea039a 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 f1232b5f862..dfaf33b3969 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 4060808c268..4f2e8952566 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 9f18963dcb9..387dbe7b26c 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 c4b70e90076..c4091388b35 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 5e0537b6633..97bf9fc498f 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 923fbab2440..db992a56305 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 9729938035d..e4c864d9fd3 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 c5368b675b3..ddd6ef4cdb2 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 00000000000..1dcdb7fe5af --- /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 59dff606f8f..d0d0b9114fb 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 395fe5b9daa..33d3ddbfafe 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 d52e7be1282..5c9a9157ba6 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 21e926ec9e1..c90c1aefee7 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 964b143f1d5..4e04ab16ce2 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 566568ce37c..b1561285ebf 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 acfb900218b..9067e2c6f53 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 d04e5796542..4454bd940d4 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 401c5a540d6..32802248856 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 5e378369465..f1307e2a169 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 41bb76282b0..ca8c6f81eda 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 e66bf3b4043..31890269925 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 4b9fe3b564d..f9b64dc8483 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 2bb48d39f29..57bc6e68436 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 39f14020aec..88fe5330ee0 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 eee2386a921..8e2ec3d36c0 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 00000000000..f46e17923ad --- /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 00000000000..c76efd0e898 --- /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 95bd22cbecf..649de0f86e4 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 e5a72f35e2b..a64a64ab74e 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 5fc7e1050ae..2dd7407ad92 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 49ccd378c1d..4736fc91bf0 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 a594ffe3545..1dd51cd7e62 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 50a9b5fedbf..b40ea7917f8 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 152999b5574..3034135f847 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 4593a392ee3..f4d7c5dccfa 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 97c9b589c48..8de4e76c6f6 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 00f3c2da2cb..8e6b036202f 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 dac983b1acd..daf44d50e03 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 bf0074d3191..adb344ac47d 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 3aa70aa48b3..6c1d9312f91 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 cd394234ccf..d29ab71cff8 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 31085292fbb..44cbd69bcd2 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 daf44d50e03..210e440a158 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 adb344ac47d..e8e173e1b6b 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 9f522b086fc..7a18da03ddc 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 6976e26ebb2..3d967b25c15 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 fc1f0c9a707..89f60faf84e 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 d5a61fd18ae..54b70d78673 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 210e440a158..1ba5d3d2000 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 e8e173e1b6b..b6613a840fe 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 92d8d2638b9..77fdfb7d48a 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 94396bbf883..6bb7dc1a870 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 48664bff395..00633422939 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 27e62fafc73..ebe34c2aa95 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 b097aec56af..a271ca666f4 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 b40bc6d1971..75615c1cce7 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 89fb17affc8..69790e71732 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 c17667cbed2..b1d791e3284 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 d9f0f9c0d3a..50d067bf29a 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 d29ab71cff8..a33cd59f227 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 218bb1132a3..00000000000 --- 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 242a90088b3..ab09df87ae3 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 a8459a2cd74..f637b641490 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 151b1dac000..e9bffa2706c 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 56d4ac31b78..dc58dd2856f 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 dfa365a36c3..f0e0b8e05fa 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 6e457da039c..a74a50b0f36 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 00000000000..218b01fd71b --- /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 00000000000..4470bd14676 --- /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 48d9cb00783..cc9be9d7588 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 ee28a0905c3..cd0d66acba8 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 00000000000..664606b9edc --- /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 8be2693325e..f5fb286df7e 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 2df668513e1..34f525ca7a6 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 6245c5d83d0..0dcc89e2262 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 491b9bd9e07..ecc37bbe244 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 5a13056e685..fe0319764a7 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 19768cad7db..743a68a010e 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 4c726019424..464bad99d5a 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 7c5675dad31..04b25ad7cb8 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 35636c0fd95..737fbc5ff53 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 0d6bcb67479..95c0f1734cc 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 bb1ef3e2c03..db3aa75462a 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 22895399156..493ceb4dfd1 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 00000000000..dd1acc230f1 --- /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 426dbbe399c..94fcd3c4cb2 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 abe1d21c90a..b88a9d0cfa4 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 400fd8eb933..2bc328ff8f6 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 da5d717cb37..4c0b8b2617c 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 815b87f2ec0..70eaaaf18df 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 1e224e5ac51..8ee83eab727 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 5771c9da9a9..2fdeb991bfd 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 d3fe05bce10..ec9b5681e76 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 b6a10d7cde5..bfd393bbbb8 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 768f114ac4f..a1f6f595a04 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 4f4dd8a0956..d9060a719d8 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 7d6c6222d2e..5f5e8ea6221 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 1ba5d3d2000..1e3c34e1912 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 b6613a840fe..dcdd3df923f 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 72929e1ecb7..95acd6a0c15 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 fbee7d1b439..497f11760fe 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 c80c8f881cd..e5a84041d35 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 4273be3f435..c1895aa5665 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 e7e665d65f9..14dd36ce5b0 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 84cf711ac97..ce781266a6e 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 81d555806ed..0c31ca070cf 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 1e3c34e1912..68e2c5187d8 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 dcdd3df923f..7f9c7134fdc 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 faa3e7115b8..4514beda8c0 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 a054d863c9e..9177b1deb7c 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 aa54e582af0..3f6a142e281 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 00000000000..f41ee93aefe --- /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 71bea5a4d88..e1c89501394 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 00000000000..997d60402ca --- /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 dc8ff4ec511..36dbb530fdb 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 2bf2dd68b44..8eaa5185eb9 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 99c0652d982..a28c8bdbbb8 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 00000000000..e38e7f99169 --- /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 f01728783cc..4bbc28bfedd 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 277c044c533..338a019019f 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 e2607c5f362..2733d6bd12d 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 00000000000..6829f43011a --- /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 d42b858157b..8c0c5441a92 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 00000000000..0108cb98d64 --- /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 00000000000..ca8388f5bec --- /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 9d5bdcfa20a..17a20705cd4 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 777776ce42d..e0810147cda 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 00000000000..bb61b035201 --- /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 3ab2d0fd09d..96af3b0453f 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 9ab94c993eb..843ce983305 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 4af5404c63b..25660b4805e 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 c4d67feb5bf..8da77910c38 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 2fd65bb972e..3c928935cce 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 cae09d77621..480780db0a9 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 7b4c1e71d57..869599296d5 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 3f6e78fd3b4..3414e37a657 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 703ac8614c4..0c0c184b5fe 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 0b138211240..a56222e22eb 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 9feea6d6288..ec945015278 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 31529d69a2d..26c57268689 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 1883a5ab055..3a015b5ad6c 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 4879603af6c..b07ad70adac 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 72300c1621e..97d2b594cc3 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 8b779696a70..1c3b4b0d672 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 6d0f5f22d79..54e2e9f92a8 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 e371fa6fe25..acb45bca85f 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 d9d6a16ebf7..32913c37224 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 8eb8b0d9a32..573de21c692 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 87a6ff39552..2af91c0fe32 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 d6231efe7ad..1142a476bc5 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 c4abc12d240..00a2738f4fe 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 4542506bec5..e7308558127 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 3d7b1b8fc13..1becc929adc 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 d614776778e..14328953367 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 00000000000..2b9eefdbb6b --- /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 797aac82405..5f14558e2b5 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 4d9fe2eba2a..ca685f36a13 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 e986913fc70..dc04926004a 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 68e2c5187d8..bddeb3096ef 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 7f9c7134fdc..0622f6a788f 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 de7b8b8f0d7..5e9b4f8e690 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 280956c7ddd..1e16675f7b6 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 bddeb3096ef..efdef2dc283 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 78429dd1fe0..2fc83ea71de 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 0de30d43c65..cf8323d2e81 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 fd574784838..b0cd7bb8b8d 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 efdef2dc283..4dfd64840b5 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 0622f6a788f..47c92051e13 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 69dac297b1b..34c2909188f 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 1e16675f7b6..1273b720dd8 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 c2846f8c57f..22711b84b9d 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 4dfd64840b5..99f972dc590 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 47c92051e13..9385f6956de 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 d1c12e657fd..fb9560832ca 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 f637b641490..64a22ef275f 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 e5d67234f6f..66c5801e39e 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 83cd4232d42..00000000000 --- 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 22798b09689..00000000000 --- 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 f3b659ddccd..00000000000 --- 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 987b3679b3c..00000000000 --- 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 dab3491b356..00000000000 --- 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 8a418bfb7bd..00000000000 --- 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 06519a5c455..00000000000 --- 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 e00409ffa26..00000000000 --- 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 b23bf7e8970..00000000000 --- 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 77edbd49901..00000000000 --- 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 e46abc942c9..00000000000 --- 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 fc0ece086a7..00000000000 --- 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 832849a7060..00000000000 --- 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 11539723fab..00000000000 --- 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 0e83b893642..00000000000 --- 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 188f82beae9..00000000000 --- 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 50c92e0678a..00000000000 --- 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 ab6504519d4..00000000000 --- 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 657640c2beb..00000000000 --- 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 a5be62bfc13..00000000000 --- 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 3f1edaf4c7d..00000000000 --- 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 dfe3f85a38d..00000000000 --- 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 77ddb25fb6e..00000000000 --- 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 01003d0e9d0..00000000000 --- 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 566a9e43dc0..00000000000 --- 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 833f11190b6..11a0b517646 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 99f972dc590..ec7b256775c 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 9385f6956de..6fc1b3da392 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 c5212855038..00000000000 --- 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 2dba3c3c11d..00000000000 --- 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 912deb7ffea..f0899598351 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 cc1877fb5bb..0c53fbd6079 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 d2cec93df95..d57828fdfa4 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 039002ca7a7..9ae49419bf4 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 ceccc7d8c39..76a9a5bb070 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 e9bffa2706c..ab235d7559a 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 37bd2fed8a6..a643de0e6c9 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 368eb8b3465..08cb0161bd9 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 028f56587a1..7d60bb1cf5d 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 9009414c5b4..52490f41f86 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 ec7b256775c..917006cea97 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 6fc1b3da392..485caf8bfce 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 cd3a06a5afa..d7b78b98f8f 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 3199b89f21c..29091f32f51 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 44a0f551ae0..ab64183a142 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 00000000000..852ff6ae544 --- /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 3e0c79ad65e..438b7a21394 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 9fc7b198054..95c524b0433 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 d91896a4e12..fa2ce2f3aff 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 56be5eb0123..a937aea25e4 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 1a99db5c24c..2755d14a3a3 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 00000000000..39dd068f5a6 --- /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 2ea2c0508ee..646e7741530 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 ab235d7559a..e4be96cbf14 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 d7b78b98f8f..65a37563911 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 e506f269d66..aad5621935e 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 00000000000..cb17f81d18a --- /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 e7dad0885a0..f06d815e5c5 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 42fc5dc7a49..2c88533f30d 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 ab64183a142..228ac48bcb2 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 852ff6ae544..279633b07b1 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 438b7a21394..423205f15b2 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 fa2ce2f3aff..03900fdeeb5 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 a937aea25e4..4e8f69b17c8 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 2755d14a3a3..3fcdb6426fb 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 64a22ef275f..c16b6ecb986 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 93ee4fc9b8f..3439684f977 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 00000000000..2d37a0b0dce --- /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 b5990dede21..73fe0f2152d 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 11f83d80179..b7fd193afad 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 40313a41553..084811c8fa0 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 1aa6bcdea75..00f193f8663 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 321dce9a296..6c385181aaa 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 f06d815e5c5..463b1c65a98 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 1bfae6cb900..39cd7127402 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 812ac168d48..c57c7269723 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 01c4047af65..6b657f600d8 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 ddd1847f6a8..7b38c102253 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 6b3a39d0926..4a25fa3c988 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 95acd6a0c15..2f087cbe523 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 e5a84041d35..27f2310c7ce 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 c1895aa5665..06c1f1d2735 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 00000000000..f46cc94bd43 --- /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 96f2401e8d7..92b56a4804e 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 44adaf970b8..3fbf4a21fb3 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 724903ef360..6a33e3341b0 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 95435bb3bd9..9c645062256 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 ad8f0f41ae7..9f3518bcf8d 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 00000000000..f4e9792a589 --- /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 9f3946e6ade..534168fa78e 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 00000000000..89a8b224f1a --- /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 9e3c4d41229..a896b37a26b 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 237733e6870..8ec536e7331 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 59f14ab2ee5..8da530bebda 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 3ff1577c436..2c3e2f7071d 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 558f4adced8..066cee61c05 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 b2f8e272983..4a20aa651cd 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 917006cea97..5bb2f37d9a0 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 485caf8bfce..7ff9bd8c974 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 a7be6ddcf6b..e0f4c6eda99 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 b339013a69f..87491a72546 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 a2b50f20eb6..64b4fdf66ad 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 5bb2f37d9a0..9504cba0d5f 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 87491a72546..9e9760560d5 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 9504cba0d5f..6fc710ab28a 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 c16b6ecb986..9f2fdc80716 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 c72d1ce40fe..e9f6a0d7f6f 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 31ef2dacf3a..cad5426d548 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 00000000000..a6081d5ccc1 --- /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 d1a44b5e5c9..65dc7fde595 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 fa732b64e77..7f069e741b8 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 d30f1702ae8..0d281e78f14 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 cb17459e105..5bce904e1cd 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 9cf2bca2df5..3fd40f3801c 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 4668b1da6eb..480aac34eb3 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 00000000000..c0c45b814a4 --- /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 00000000000..f194c8a3444 --- /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 11a0b517646..9e204e91da5 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 7ff9bd8c974..73503ac0e17 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 00000000000..e1243fe2c0a --- /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 00000000000..ae32feec7b1 --- /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 06c1f1d2735..2b42c1be666 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 0c31ca070cf..dd637696e77 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 70d55a74b98..d1474c3cf5f 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 65b70a8463f..9d50e62fcd1 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 5270ac69704..3f12eca0f5f 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 60dc293c4fd..59d65977066 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 2f087cbe523..e10f1655d2f 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 2b42c1be666..3087401c3ae 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 1b97b800956..4d621d18fa6 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 6fc710ab28a..ea6b464f66b 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 497f11760fe..cb85b45e0e0 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 d9c8a4099ac..d922923d472 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 4514beda8c0..4a25026959c 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 5d748edb841..80ad585486b 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 828cf1fb7b4..822a274bf23 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 caf309e6718..abe98b98c8c 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 7cdec1fd7b8..7eb736c0037 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 ea6b464f66b..c7d955b8b41 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 73503ac0e17..2e038049fad 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 32913c37224..75ca322b9a3 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 2a51c6ffd83..02a60049f07 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 c7d955b8b41..5209f0b29d9 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 6e494ce2692..f8e9034292c 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 39b48b91a84..682576b534a 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 6d9000b8669..55b533d4f1c 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 6efa44e304f..57b6e6cb123 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 f2da007b5e0..4ab2991bd43 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 a3cda6afa69..16e4a434dc3 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 339b5750036..db78e5ce0e9 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 5209f0b29d9..8e0dd465bf5 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 66c5801e39e..a660d930128 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 8cd8b0672cf..2348d42a0d3 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 239396b2d0c..fff58433a9c 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 73b27ee7d2a..a69e1fb95d8 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 00000000000..89dcfa396aa --- /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 28efb64c71e..228c8c6c176 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 13c29364975..6260f4a9ffc 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 437e45ba72a..ac7c8f40e8d 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 526a774cc39..c4d7de3839e 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 157bbf3bc23..f2f2db37d7f 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 3b08a7afe14..9ea39b63888 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 8e0dd465bf5..565b6b4c34a 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 e4be96cbf14..1240d30f027 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 29091f32f51..b7c75862153 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 12314f60561..7295d134087 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 298c7e111b2..cd08b625a5d 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 3c07fbc4e20..2c17d85f7b3 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 565b6b4c34a..8477f615aab 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 de18fd4ed04..2ff32fc8392 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 c2c383468f6..67649b7edba 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 8477f615aab..cdf9eedec8f 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 2e038049fad..22246aa4ece 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 61c9ca54854..4c06c89692a 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 e966188afd2..5f33aa2be4a 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 996d46e08a7..0d11ec2289c 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 53038352d4d..d53f089054a 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 d2c23f46093..a71c124c633 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 77105dc73db..c4e4c91087c 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 b4598d64087..af6b0ce54ba 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 cdf9eedec8f..4369011a58a 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 22246aa4ece..0153cfee3db 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 ed2460614fc..ebca96da5fd 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 4369011a58a..e74131e3479 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 0153cfee3db..7d4bf31e2e0 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 b3a87510e5a..039513f100e 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 e74131e3479..26b139a905c 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 9a4ed9e7782..6b25ec7d123 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 261c1898ca9..b3dd7bb80fe 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 71c971757c1..2850be11450 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 dc58dd2856f..d49ec8535cc 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 4470bd14676..32c30f747d2 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 00000000000..f85fcdaa749 --- /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 0e534147cb1..9bb9ba00261 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 68300adbcfe..fdf4b98faf8 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 57b6e6cb123..49527666f53 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 389e380af85..7500483ec53 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 8767953b363..6b79c552911 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 ebca96da5fd..54fceda03a6 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 26b139a905c..b238a797135 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 7d4bf31e2e0..4c9dc5cc37e 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 fcda7b0bb67..41c1f52b993 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 661e1a11b56..f06ac8aca3f 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 48a662e3a81..77c842a27fe 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 1b9ddc52191..9c7ddb09f85 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 92666335f28..c81ed681d42 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 3e4f97d5bc7..15b871cde5c 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 15b871cde5c..f15df428640 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 7eb736c0037..6f25618871f 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 f627346c67b..130fd2cc245 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 2ff32fc8392..8a6d0f58937 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 0c53fbd6079..78cd317bb3e 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 cd08b625a5d..147d9fb950d 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 cd747466973..9f5c5b2efc7 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 c633c114b46..4501b25385e 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 5b37f7e3f9d..9481e954bde 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 49cee1ff280..a7ce097ae84 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 d244c316483..5c77add3118 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 1d547e895bf..31230315954 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 8a6d0f58937..dff0d120c9d 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 753ac2a2441..654eec820c3 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 6f25618871f..1c9ddc70f50 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 b238a797135..005c6e03e93 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 4c9dc5cc37e..96b7618c1d5 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 4a25fa3c988..32b8a064054 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 72f11b7b005..cc1e47d71fc 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 5d712045c93..415668f5924 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 e3eb92a426f..ece3994f651 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 c5e407573ba..3d7c2a06a3b 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 c6ef895b7df..5242ed1cc59 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 f07c4d9c646..919051d7e7a 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 26b54def974..4d4be5e1259 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 5891629627e..6f9cc25db99 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 005c6e03e93..76641ab9f7a 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 7d3a3994ace..cc5c604dc8c 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 574ba6b8ba7..c05d9641b9a 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 41ebfb340eb..41738c824fb 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 1bb74053ea4..7abbefe85ff 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 2a2d383f362..155a39e49b6 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 4b5891359b6..2ac0bc40252 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 3075f6a3f9d..a4d8b84b1ad 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 d9c1a5a40cd..bc007fefb84 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 6b48c313fcc..45991375ba0 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 e54a5dcde01..ce761952921 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 fe461654ecd..da6765368ae 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 76641ab9f7a..2486c7317b1 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 fd2f08c1488..0493a964cc4 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 abe98b98c8c..241b07fd591 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 1c9ddc70f50..11e7dd89918 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 2486c7317b1..0cf2451432e 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 96b7618c1d5..70249117ee3 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 dff0d120c9d..52254ad68e9 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 57f58f05993..5c6bf76a169 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 0cf2451432e..4d1bd88a62e 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 70249117ee3..278822f3c07 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 4d765cc0508..24e37216b70 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 38d6b4577b6..040744b27aa 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 4d1bd88a62e..cc6cd185743 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 278822f3c07..466c072f4a9 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 52254ad68e9..4baa7e7bc3a 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 cc1e47d71fc..72f11b7b005 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 415668f5924..5d712045c93 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 ece3994f651..e3eb92a426f 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 3d7c2a06a3b..c5e407573ba 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 5242ed1cc59..c6ef895b7df 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 919051d7e7a..f07c4d9c646 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 4d4be5e1259..26b54def974 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 6f9cc25db99..5891629627e 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 cc6cd185743..8e3b891c2e7 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